From 1101a147688e316e6d1569853a07fa708400ddeb Mon Sep 17 00:00:00 2001 From: Chia-hung Tai Date: Thu, 21 Feb 2013 18:28:35 +0800 Subject: [PATCH] Bug 833697 - Transaction-based MmsService. r=vyang --- dom/mms/src/ril/MmsService.js | 1289 +++++++++++++++------------ dom/mms/src/ril/MmsService.manifest | 1 - dom/mms/src/ril/WspPduHelper.jsm | 46 +- 3 files changed, 742 insertions(+), 594 deletions(-) diff --git a/dom/mms/src/ril/MmsService.js b/dom/mms/src/ril/MmsService.js index ddcba23e85d4..53ced4e77d51 100644 --- a/dom/mms/src/ril/MmsService.js +++ b/dom/mms/src/ril/MmsService.js @@ -1,4 +1,6 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 et filetype=javascript + * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ @@ -50,83 +52,670 @@ XPCOMUtils.defineLazyGetter(this, "MMS", function () { return MMS; }); +XPCOMUtils.defineLazyGetter(this, "gMmsConnection", function () { + let conn = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + /** MMS proxy settings. */ + mmsc: null, + proxy: null, + port: null, + + proxyInfo: null, + settings: ["ril.mms.mmsc", + "ril.mms.mmsproxy", + "ril.mms.mmsport"], + connected: false, + + //A queue to buffer the MMS HTTP requests when the MMS network + //is not yet connected. The buffered requests will be cleared + //if the MMS network fails to be connected within a timer. + pendingCallbacks: [], + + /** MMS network connection reference count. */ + refCount: 0, + + connectTimer: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer), + + disconnectTimer: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer), + + /** + * Callback when |connectTimer| is timeout or cancelled by shutdown. + */ + onConnectTimerTimeout: function onConnectTimerTimeout() { + debug("onConnectTimerTimeout: " + this.pendingCallbacks.length + + " pending callbacks"); + while (this.pendingCallbacks.length) { + let callback = this.pendingCallbacks.shift(); + callback(false); + } + }, + + /** + * Callback when |disconnectTimer| is timeout or cancelled by shutdown. + */ + onDisconnectTimerTimeout: function onDisconnectTimerTimeout() { + debug("onDisconnectTimerTimeout: deactivate the MMS data call."); + if (this.connected) { + gRIL.deactivateDataCallByType("mms"); + } + }, + + init: function init() { + Services.obs.addObserver(this, kNetworkInterfaceStateChangedTopic, + false); + Services.obs.addObserver(this, kXpcomShutdownObserverTopic, false); + this.settings.forEach(function(name) { + Services.prefs.addObserver(name, this, false); + }, this); + + try { + this.mmsc = Services.prefs.getCharPref("ril.mms.mmsc"); + this.proxy = Services.prefs.getCharPref("ril.mms.mmsproxy"); + this.port = Services.prefs.getIntPref("ril.mms.mmsport"); + this.updateProxyInfo(); + } catch (e) { + debug("Unable to initialize the MMS proxy settings from the" + + "preference. This could happen at the first-run. Should be" + + "available later."); + this.clearMmsProxySettings(); + } + }, + + /** + * Acquire the MMS network connection. + * + * @param callback + * Callback function when either the connection setup is done, + * timeout, or failed. Accepts a boolean value that indicates + * whether the connection is ready. + * + * @return true if the MMS network connection is already acquired and the + * callback is done; false otherwise. + */ + acquire: function acquire(callback) { + this.connectTimer.cancel(); + + // If the MMS network is not yet connected, buffer the + // MMS request and try to setup the MMS network first. + if (!this.connected) { + debug("acquire: buffer the MMS request and setup the MMS data call."); + this.pendingCallbacks.push(callback); + gRIL.setupDataCallByType("mms"); + + // Set a timer to clear the buffered MMS requests if the + // MMS network fails to be connected within a time period. + this.connectTimer. + initWithCallback(this.onConnectTimerTimeout.bind(this), + TIME_TO_BUFFER_MMS_REQUESTS, + Ci.nsITimer.TYPE_ONE_SHOT); + return false; + } + + this.refCount++; + + callback(true); + return true; + }, + + /** + * Release the MMS network connection. + */ + release: function release() { + this.refCount--; + if (this.refCount <= 0) { + this.refCount = 0; + + // Set a timer to delay the release of MMS network connection, + // since the MMS requests often come consecutively in a short time. + this.disconnectTimer. + initWithCallback(this.onDisconnectTimerTimeout.bind(this), + TIME_TO_RELEASE_MMS_CONNECTION, + Ci.nsITimer.TYPE_ONE_SHOT); + } + }, + + /** + * Update the MMS proxy info. + */ + updateProxyInfo: function updateProxyInfo() { + if (this.proxy === null || this.port === null) { + debug("updateProxyInfo: proxy or port is not yet decided." ); + return; + } + + this.proxyInfo = + gpps.newProxyInfo("http", this.proxy, this.port, + Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST, + -1, null); + debug("updateProxyInfo: " + JSON.stringify(this.proxyInfo)); + }, + + /** + * Clear the MMS proxy settings. + */ + clearMmsProxySettings: function clearMmsProxySettings() { + this.mmsc = null; + this.proxy = null; + this.port = null; + this.proxyInfo = null; + }, + + shutdown: function shutdown() { + Services.obs.removeObserver(this, kNetworkInterfaceStateChangedTopic); + this.settings.forEach(function(name) { + Services.prefs.removeObserver(name, this); + }, this); + this.connectTimer.cancel(); + this.onConnectTimerTimeout(); + this.disconnectTimer.cancel(); + this.onDisconnectTimerTimeout(); + }, + + // nsIObserver + + observe: function observe(subject, topic, data) { + switch (topic) { + case kNetworkInterfaceStateChangedTopic: { + this.connected = + gRIL.getDataCallStateByType("mms") == + Ci.nsINetworkInterface.NETWORK_STATE_CONNECTED; + + if (!this.connected) { + return; + } + + debug("Got the MMS network connected! Resend the buffered " + + "MMS requests: number: " + this.pendingCallbacks.length); + this.connectTimer.cancel(); + while (this.pendingCallbacks.length) { + let callback = this.pendingCallbacks.shift(); + callback(true); + } + break; + } + case kPrefenceChangedObserverTopic: { + try { + switch (data) { + case "ril.mms.mmsc": + this.mmsc = Services.prefs.getCharPref("ril.mms.mmsc"); + break; + case "ril.mms.mmsproxy": + this.proxy = Services.prefs.getCharPref("ril.mms.mmsproxy"); + this.updateProxyInfo(); + break; + case "ril.mms.mmsport": + this.port = Services.prefs.getIntPref("ril.mms.mmsport"); + this.updateProxyInfo(); + break; + default: + break; + } + } catch (e) { + debug("Failed to update the MMS proxy settings from the" + + "preference."); + this.clearMmsProxySettings(); + } + break; + } + case kXpcomShutdownObserverTopic: { + Services.obs.removeObserver(this, kXpcomShutdownObserverTopic); + this.shutdown(); + } + } + } + }; + conn.init(); + + return conn; +}); + +function MmsProxyFilter(url) { + this.url = url; +} +MmsProxyFilter.prototype = { + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolProxyFilter]), + + // nsIProtocolProxyFilter + + applyFilter: function applyFilter(proxyService, uri, proxyInfo) { + let url = uri.prePath + uri.path; + if (url.endsWith("/")) { + url = url.substr(0, url.length - 1); + } + + if (this.url != url) { + debug("applyFilter: content uri = " + this.url + + " is not matched url = " + url + " ."); + return proxyInfo; + } + // Fall-through, reutrn the MMS proxy info. + debug("applyFilter: MMSC is matched: " + + JSON.stringify({ url: this.url, + proxyInfo: gMmsConnection.proxyInfo })); + return gMmsConnection.proxyInfo ? gMmsConnection.proxyInfo : proxyInfo; + } +}; + +XPCOMUtils.defineLazyGetter(this, "gMmsTransactionHelper", function () { + return { + /** + * Send MMS request to MMSC. + * + * @param method + * "GET" or "POST". + * @param url + * Target url string. + * @param istream + * An nsIInputStream instance as data source to be sent or null. + * @param callback + * A callback function that takes two arguments: one for http + * status, the other for wrapped PDU data for further parsing. + */ + sendRequest: function sendRequest(method, url, istream, callback) { + // TODO: bug 810226 - Support of GPRS bearer for MMS transmission and + // reception + + gMmsConnection.acquire((function (method, url, istream, callback, + connected) { + if (!connected) { + // Connection timeout or failed. Report error. + gMmsConnection.release(); + if (callback) { + callback(0, null); + } + return; + } + + debug("sendRequest: register proxy filter to " + url); + let proxyFilter = new MmsProxyFilter(url); + gpps.registerFilter(proxyFilter, 0); + + let releaseMmsConnectionAndCallback = (function (httpStatus, data) { + gpps.unregisterFilter(proxyFilter); + // Always release the MMS network connection before callback. + gMmsConnection.release(); + if (callback) { + callback(httpStatus, data); + } + }).bind(this); + + try { + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + + // Basic setups + xhr.open(method, url, true); + xhr.responseType = "arraybuffer"; + if (istream) { + xhr.setRequestHeader("Content-Type", + "application/vnd.wap.mms-message"); + xhr.setRequestHeader("Content-Length", istream.available()); + } else { + xhr.setRequestHeader("Content-Length", 0); + } + + // UAProf headers. + let uaProfUrl, uaProfTagname = "x-wap-profile"; + try { + uaProfUrl = Services.prefs.getCharPref('wap.UAProf.url'); + uaProfTagname = Services.prefs.getCharPref('wap.UAProf.tagname'); + } catch (e) {} + + if (uaProfUrl) { + xhr.setRequestHeader(uaProfTagname, uaProfUrl); + } + + // Setup event listeners + xhr.onerror = function () { + debug("xhr error, response headers: " + + xhr.getAllResponseHeaders()); + releaseMmsConnectionAndCallback(xhr.status, null); + }; + xhr.onreadystatechange = function () { + if (xhr.readyState != Ci.nsIXMLHttpRequest.DONE) { + return; + } + + let data = null; + switch (xhr.status) { + case HTTP_STATUS_OK: { + debug("xhr success, response headers: " + + xhr.getAllResponseHeaders()); + + let array = new Uint8Array(xhr.response); + if (false) { + for (let begin = 0; begin < array.length; begin += 20) { + let partial = array.subarray(begin, begin + 20); + debug("res: " + JSON.stringify(partial)); + } + } + + data = {array: array, offset: 0}; + break; + } + default: { + debug("xhr done, but status = " + xhr.status); + break; + } + } + + releaseMmsConnectionAndCallback(xhr.status, data); + } + + // Send request + xhr.send(istream); + } catch (e) { + debug("xhr error, can't send: " + e.message); + releaseMmsConnectionAndCallback(0, null); + } + }).bind(this, method, url, istream, callback)); + } + }; +}); + +/** + * Send M-NotifyResp.ind back to MMSC. + * + * @param transactionId + * X-Mms-Transaction-ID of the message. + * @param status + * X-Mms-Status of the response. + * @param reportAllowed + * X-Mms-Report-Allowed of the response. + * + * @see OMA-TS-MMS_ENC-V1_3-20110913-A section 6.2 + */ +function NotifyResponseTransaction(transactionId, status, reportAllowed) { + let headers = {}; + + // Mandatory fields + headers["x-mms-message-type"] = MMS.MMS_PDU_TYPE_NOTIFYRESP_IND; + headers["x-mms-transaction-id"] = transactionId; + headers["x-mms-mms-version"] = MMS.MMS_VERSION; + headers["x-mms-status"] = status; + // Optional fields + headers["x-mms-report-allowed"] = reportAllowed; + + this.istream = MMS.PduHelper.compose(null, {headers: headers}); +} +NotifyResponseTransaction.prototype = { + /** + * @param callback [optional] + * A callback function that takes one argument -- the http status. + */ + run: function run(callback) { + let requestCallback; + if (callback) { + requestCallback = function (httpStatus, data) { + // `The MMS Client SHOULD ignore the associated HTTP POST response + // from the MMS Proxy-Relay.` ~ OMA-TS-MMS_CTR-V1_3-20110913-A + // section 8.2.2 "Notification". + callback(httpStatus); + }; + } + gMmsTransactionHelper.sendRequest("POST", gMmsConnection.mmsc, + this.istream, requestCallback); + } +}; + +/** + * Retrieve message back from MMSC. + * + * @param contentLocation + * X-Mms-Content-Location of the message. + */ +function RetrieveTransaction(contentLocation) { + this.contentLocation = contentLocation; +} +RetrieveTransaction.prototype = { + /** + * @param callback [optional] + * A callback function that takes two arguments: one for X-Mms-Status, + * the other for the parsed M-Retrieve.conf message. + */ + run: function run(callback) { + let callbackIfValid = function callbackIfValid(status, msg) { + if (callback) { + callback(status, msg); + } + } + + gMmsTransactionHelper.sendRequest("GET", this.contentLocation, null, + (function (httpStatus, data) { + if ((httpStatus != HTTP_STATUS_OK) || !data) { + callbackIfValid(MMS.MMS_PDU_STATUS_DEFERRED, null); + return; + } + + let retrieved = MMS.PduHelper.parse(data, null); + if (!retrieved || (retrieved.type != MMS.MMS_PDU_TYPE_RETRIEVE_CONF)) { + callbackIfValid(MMS.MMS_PDU_STATUS_UNRECOGNISED, null); + return; + } + + // Fix default header field values. + if (retrieved.headers["x-mms-delivery-report"] == null) { + retrieved.headers["x-mms-delivery-report"] = false; + } + + let retrieveStatus = retrieved.headers["x-mms-retrieve-status"]; + if ((retrieveStatus != null) && + (retrieveStatus != MMS.MMS_PDU_ERROR_OK)) { + callbackIfValid(MMS.translatePduErrorToStatus(retrieveStatus), + retrieved); + return; + } + + callbackIfValid(MMS.MMS_PDU_STATUS_RETRIEVED, retrieved); + }).bind(this)); + } +}; + +/** + * SendTransaction. + * Class for sending M-Send.req to MMSC + */ +function SendTransaction(msg) { + msg.headers["x-mms-message-type"] = MMS.MMS_PDU_TYPE_SEND_REQ; + if (!msg.headers["x-mms-transaction-id"]) { + // Create an unique transaction id + let tid = gUUIDGenerator.generateUUID().toString(); + msg.headers["x-mms-transaction-id"] = tid; + } + msg.headers["x-mms-mms-version"] = MMS.MMS_VERSION; + + // Let MMS Proxy Relay insert from address automatically for us + msg.headers["from"] = null; + + msg.headers["date"] = new Date(); + msg.headers["x-mms-message-class"] = "personal"; + msg.headers["x-mms-expiry"] = 7 * 24 * 60 * 60; + msg.headers["x-mms-priority"] = 129; + msg.headers["x-mms-read-report"] = true; + msg.headers["x-mms-delivery-report"] = true; + + // TODO: bug 792321 - MMSCONF-GEN-C-003: Support for maximum values for MMS + // parameters + + let messageSize = 0; + + if (msg.content) { + messageSize = msg.content.length; + } else if (msg.parts) { + for (let i = 0; i < msg.parts.length; i++) { + if (msg.parts[i].content.size) { + messageSize += msg.parts[i].content.size; + } else { + messageSize += msg.parts[i].content.length; + } + } + + let contentType = { + params: { + // `The type parameter must be specified and its value is the MIME + // media type of the "root" body part.` ~ RFC 2387 clause 3.1 + type: msg.parts[0].headers["content-type"].media, + }, + }; + + // `The Content-Type in M-Send.req and M-Retrieve.conf SHALL be + // application/vnd.wap.multipart.mixed when there is no presentation, and + // application/vnd.wap.multipart.related SHALL be used when there is SMIL + // presentation available.` ~ OMA-TS-MMS_CONF-V1_3-20110913-A clause 10.2.1 + if (contentType.params.type === "application/smil") { + contentType.media = "application/vnd.wap.multipart.related"; + + // `The start parameter, if given, is the content-ID of the compound + // object's "root".` ~ RFC 2387 clause 3.2 + contentType.params.start = msg.parts[0].headers["content-id"]; + } else { + contentType.media = "application/vnd.wap.multipart.mixed"; + } + + // Assign to Content-Type + msg.headers["content-type"] = contentType; + } + + // Assign to X-Mms-Message-Size + msg.headers["x-mms-message-size"] = messageSize; + // TODO: bug 809832 - support customizable max incoming/outgoing message size + + debug("msg: " + JSON.stringify(msg)); + + this.msg = msg; +} +SendTransaction.prototype = { + istreamComposed: false, + + /** + * @param parts + * 'parts' property of a parsed MMS message. + * @param callback [optional] + * A callback function that takes zero argument. + */ + loadBlobs: function loadBlobs(parts, callback) { + let callbackIfValid = function callbackIfValid() { + debug("All parts loaded: " + JSON.stringify(parts)); + if (callback) { + callback(); + } + } + + if (!parts || !parts.length) { + callbackIfValid(); + return; + } + + let numPartsToLoad = parts.length; + for each (let part in parts) { + if (!(part.content instanceof Ci.nsIDOMBlob)) { + numPartsToLoad--; + if (!numPartsToLoad) { + callbackIfValid(); + return; + } + continue; + } + let fileReader = Cc["@mozilla.org/files/filereader;1"] + .createInstance(Ci.nsIDOMFileReader); + fileReader.addEventListener("loadend", + (function onloadend(part, event) { + let arrayBuffer = event.target.result; + part.content = new Uint8Array(arrayBuffer); + numPartsToLoad--; + if (!numPartsToLoad) { + callbackIfValid(); + } + }).bind(null, part)); + fileReader.readAsArrayBuffer(part.content); + }; + }, + + /** + * @param callback [optional] + * A callback function that takes two arguments: one for + * X-Mms-Response-Status, the other for the parsed M-Send.conf message. + */ + run: function run(callback) { + if (!this.istreamComposed) { + this.loadBlobs(this.msg.parts, (function () { + this.istream = MMS.PduHelper.compose(null, this.msg); + this.istreamComposed = true; + this.run(callback); + }).bind(this)); + return; + } + + let callbackIfValid = function callbackIfValid(mmsStatus, msg) { + if (callback) { + callback(mmsStatus, msg); + } + } + + if (!this.istream) { + callbackIfValid(MMS.MMS_PDU_ERROR_PERMANENT_FAILURE, null); + return; + } + + gMmsTransactionHelper.sendRequest("POST", gMmsConnection.mmsc, this.istream, + function (httpStatus, data) { + if (httpStatus != HTTP_STATUS_OK) { + callbackIfValid(MMS.MMS_PDU_ERROR_TRANSIENT_FAILURE, null); + return; + } + + if (!data) { + callbackIfValid(MMS.MMS_PDU_ERROR_PERMANENT_FAILURE, null); + return; + } + + let response = MMS.PduHelper.parse(data, null); + if (!response || (response.type != MMS.MMS_PDU_TYPE_SEND_CONF)) { + callbackIfValid(MMS.MMS_PDU_RESPONSE_ERROR_UNSUPPORTED_MESSAGE, null); + return; + } + + let responseStatus = response.headers["x-mms-response-status"]; + callbackIfValid(responseStatus, response); + }); + } +}; + /** * MmsService */ function MmsService() { - Services.obs.addObserver(this, kXpcomShutdownObserverTopic, false); - Services.obs.addObserver(this, kNetworkInterfaceStateChangedTopic, false); - this.mmsProxySettings.forEach(function(name) { - Services.prefs.addObserver(name, this, false); - }, this); - - try { - this.mmsc = Services.prefs.getCharPref("ril.mms.mmsc"); - this.mmsProxy = Services.prefs.getCharPref("ril.mms.mmsproxy"); - this.mmsPort = Services.prefs.getIntPref("ril.mms.mmsport"); - this.updateMmsProxyInfo(); - } catch (e) { - debug("Unable to initialize the MMS proxy settings from the preference. " + - "This could happen at the first-run. Should be available later."); - this.clearMmsProxySettings(); - } - - try { - this.urlUAProf = Services.prefs.getCharPref('wap.UAProf.url'); - } catch (e) { - this.urlUAProf = ""; - } - try { - this.tagnameUAProf = Services.prefs.getCharPref('wap.UAProf.tagname'); - } catch (e) { - this.tagnameUAProf = "x-wap-profile"; - } + // TODO: bug 810084 - support application identifier } MmsService.prototype = { classID: RIL_MMSSERVICE_CID, QueryInterface: XPCOMUtils.generateQI([Ci.nsIMmsService, - Ci.nsIWapPushApplication, - Ci.nsIObserver, - Ci.nsIProtocolProxyFilter]), - - /** + Ci.nsIWapPushApplication]), + /* * Whether or not should we enable X-Mms-Report-Allowed in M-NotifyResp.ind * and M-Acknowledge.ind PDU. */ confSendDeliveryReport: CONFIG_SEND_REPORT_DEFAULT_YES, - /** MMS proxy settings. */ - mmsc: null, - mmsProxy: null, - mmsPort: null, - mmsProxyInfo: null, - mmsProxySettings: ["ril.mms.mmsc", - "ril.mms.mmsproxy", - "ril.mms.mmsport"], - mmsNetworkConnected: false, - - /** MMS network connection reference count. */ - mmsConnRefCount: 0, - - // WebMMS - urlUAProf: null, - tagnameUAProf: null, - - // A queue to buffer the MMS HTTP requests when the MMS network - // is not yet connected. The buffered requests will be cleared - // if the MMS network fails to be connected within a timer. - mmsRequestQueue: [], - timerToClearQueue: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer), - - timerToReleaseMmsConnection: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer), - isProxyFilterRegistered: false, + /** + * @param status + * The MMS error type. + * + * @return true if it's a type of transient error; false otherwise. + */ + isTransientError: function isTransientError(status) { + return (status >= MMS.MMS_PDU_ERROR_TRANSIENT_FAILURE && + status < MMS.MMS_PDU_ERROR_PERMANENT_FAILURE); + }, /** * Calculate Whether or not should we enable X-Mms-Report-Allowed. * * @param config - * Current config value. + * Current configure value. * @param wish * Sender wish. Could be undefined, false, or true. */ @@ -141,483 +730,73 @@ MmsService.prototype = { }, /** - * Callback when |timerToClearQueue| is timeout or cancelled by shutdown. + * @param contentLocation + * X-Mms-Content-Location of the message. + * @param callback [optional] + * A callback function that takes two arguments: one for X-Mms-Status, + * the other parsed MMS message. */ - timerToClearQueueCb: function timerToClearQueueCb() { - debug("timerToClearQueueCb: clear the buffered MMS requests due to " + - "the timeout or cancel: number: " + this.mmsRequestQueue.length); - while (this.mmsRequestQueue.length) { - let mmsRequest = this.mmsRequestQueue.shift(); - if (mmsRequest.callback) { - mmsRequest.callback(0, null); - } - } - }, + retrieveMessage: function retrieveMessage(contentLocation, callback) { + // TODO: bug 839436 - make DB be able to save MMS messages + // TODO: bug 810099 - support onretrieving event + // TODO: bug 810097 - Retry retrieval on error + // TODO: bug 809832 - support customizable max incoming/outgoing message + // size. - /** - * Acquire the MMS network connection. - * - * @param method - * "GET" or "POST". - * @param url - * Target url string. - * @param istream [optional] - * An nsIInputStream instance as data source to be sent. - * @param callback - * A callback function that takes two arguments: one for http status, - * the other for wrapped PDU data for further parsing. - * - * @return true if the MMS network connection is acquired; false otherwise. - */ - acquireMmsConnection: function acquireMmsConnection(method, url, istream, callback) { - // If the MMS network is not yet connected, buffer the - // MMS request and try to setup the MMS network first. - if (!this.mmsNetworkConnected) { - debug("acquireMmsConnection: " + - "buffer the MMS request and setup the MMS data call."); - this.mmsRequestQueue.push({method: method, - url: url, - istream: istream, - callback: callback}); - gRIL.setupDataCallByType("mms"); - - // Set a timer to clear the buffered MMS requests if the - // MMS network fails to be connected within a time period. - this.timerToClearQueue. - initWithCallback(this.timerToClearQueueCb.bind(this), - TIME_TO_BUFFER_MMS_REQUESTS, - Ci.nsITimer.TYPE_ONE_SHOT); - return false; - } - - if (!this.mmsConnRefCount && !this.isProxyFilterRegistered) { - debug("acquireMmsConnection: register the MMS proxy filter."); - gpps.registerFilter(this, 0); - this.isProxyFilterRegistered = true; - } - this.mmsConnRefCount++; - return true; - }, - - /** - * Callback when |timerToReleaseMmsConnection| is timeout or cancelled by shutdown. - */ - timerToReleaseMmsConnectionCb: function timerToReleaseMmsConnectionCb() { - if (this.mmsConnRefCount) { - return; - } - - debug("timerToReleaseMmsConnectionCb: " + - "unregister the MMS proxy filter and deactivate the MMS data call."); - if (this.isProxyFilterRegistered) { - gpps.unregisterFilter(this); - this.isProxyFilterRegistered = false; - } - if (this.mmsNetworkConnected) { - gRIL.deactivateDataCallByType("mms"); - } - }, - - /** - * Release the MMS network connection. - */ - releaseMmsConnection: function releaseMmsConnection() { - this.mmsConnRefCount--; - if (this.mmsConnRefCount <= 0) { - this.mmsConnRefCount = 0; - - // Set a timer to delay the release of MMS network connection, - // since the MMS requests often come consecutively in a short time. - this.timerToReleaseMmsConnection. - initWithCallback(this.timerToReleaseMmsConnectionCb.bind(this), - TIME_TO_RELEASE_MMS_CONNECTION, - Ci.nsITimer.TYPE_ONE_SHOT); - } - }, - - /** - * Send MMS request to MMSC. - * - * @param method - * "GET" or "POST". - * @param url - * Target url string. - * @param istream [optional] - * An nsIInputStream instance as data source to be sent. - * @param callback - * A callback function that takes two arguments: one for http status, - * the other for wrapped PDU data for further parsing. - */ - sendMmsRequest: function sendMmsRequest(method, url, istream, callback) { - debug("sendMmsRequest: method: " + method + "url: " + url + - "istream: " + istream + "callback: " + callback); - - if (!this.acquireMmsConnection(method, url, istream, callback)) { - return; - } - - let that = this; - function releaseMmsConnectionAndCallback(status, data) { - // Always release the MMS network connection before callback. - that.releaseMmsConnection(); - if (callback) { - callback(status, data); - } - } - - try { - let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] - .createInstance(Ci.nsIXMLHttpRequest); - - // Basic setups - xhr.open(method, url, true); - xhr.responseType = "arraybuffer"; - if (istream) { - xhr.setRequestHeader("Content-Type", "application/vnd.wap.mms-message"); - xhr.setRequestHeader("Content-Length", istream.available()); - } else { - xhr.setRequestHeader("Content-Length", 0); - } - - if(this.urlUAProf !== "") { - xhr.setRequestHeader(this.tagnameUAProf, this.urlUAProf); - } - - // Setup event listeners - xhr.onerror = function () { - debug("xhr error, response headers: " + xhr.getAllResponseHeaders()); - releaseMmsConnectionAndCallback(xhr.status, null); - }; - xhr.onreadystatechange = function () { - if (xhr.readyState != Ci.nsIXMLHttpRequest.DONE) { - return; - } - - let data = null; - switch (xhr.status) { - case HTTP_STATUS_OK: { - debug("xhr success, response headers: " + xhr.getAllResponseHeaders()); - - let array = new Uint8Array(xhr.response); - if (false) { - for (let begin = 0; begin < array.length; begin += 20) { - debug("res: " + JSON.stringify(array.subarray(begin, begin + 20))); - } - } - - data = {array: array, offset: 0}; - break; - } - default: { - debug("xhr done, but status = " + xhr.status); - break; - } - } - - releaseMmsConnectionAndCallback(xhr.status, data); - } - - // Send request - xhr.send(istream); - } catch (e) { - debug("xhr error, can't send: " + e.message); - releaseMmsConnectionAndCallback(0, null); - } - }, - - /** - * Send M-NotifyResp.ind back to MMSC. - * - * @param tid - * X-Mms-Transaction-ID of the message. - * @param status - * X-Mms-Status of the response. - * @param ra - * X-Mms-Report-Allowed of the response. - * - * @see OMA-TS-MMS_ENC-V1_3-20110913-A section 6.2 - */ - sendNotificationResponse: function sendNotificationResponse(tid, status, ra) { - debug("sendNotificationResponse: tid = " + tid + ", status = " + status - + ", reportAllowed = " + ra); - - let headers = {}; - - // Mandatory fields - headers["x-mms-message-type"] = MMS.MMS_PDU_TYPE_NOTIFYRESP_IND; - headers["x-mms-transaction-id"] = tid; - headers["x-mms-mms-version"] = MMS.MMS_VERSION; - headers["x-mms-status"] = status; - // Optional fields - headers["x-mms-report-allowed"] = ra; - - let istream = MMS.PduHelper.compose(null, {headers: headers}); - this.sendMmsRequest("POST", this.mmsc, istream); - }, - - /** - * Send M-Send.req to MMSC - */ - sendSendRequest: function sendSendRequest(msg, callback) { - msg.headers["x-mms-message-type"] = MMS.MMS_PDU_TYPE_SEND_REQ; - if (!msg.headers["x-mms-transaction-id"]) { - // Create an unique transaction id - let tid = gUUIDGenerator.generateUUID().toString(); - msg.headers["x-mms-transaction-id"] = tid; - } - msg.headers["x-mms-mms-version"] = MMS.MMS_VERSION; - - // Let MMS Proxy Relay insert from address automatically for us - msg.headers["from"] = null; - - msg.headers["date"] = new Date(); - msg.headers["x-mms-message-class"] = "personal"; - msg.headers["x-mms-expiry"] = 7 * 24 * 60 * 60; - msg.headers["x-mms-priority"] = 129; - msg.headers["x-mms-read-report"] = true; - msg.headers["x-mms-delivery-report"] = true; - - let messageSize = 0; - - if (msg.content) { - messageSize = msg.content.length; - } else if (msg.parts) { - for (let i = 0; i < msg.parts.length; i++) { - messageSize += msg.parts[i].content.length; - } - - let contentType = { - params: { - // `The type parameter must be specified and its value is the MIME - // media type of the "root" body part.` ~ RFC 2387 clause 3.1 - type: msg.parts[0].headers["content-type"].media, - }, - }; - - // `The Content-Type in M-Send.req and M-Retrieve.conf SHALL be - // application/vnd.wap.multipart.mixed when there is no presentation, and - // application/vnd.wap.multipart.related SHALL be used when there is SMIL - // presentation available.` ~ OMA-TS-MMS_CONF-V1_3-20110913-A clause 10.2.1 - if (contentType.params.type === "application/smil") { - contentType.media = "application/vnd.wap.multipart.related"; - - // `The start parameter, if given, is the content-ID of the compound - // object's "root".` ~ RFC 2387 clause 3.2 - contentType.params.start = msg.parts[0].headers["content-id"]; - } else { - contentType.media = "application/vnd.wap.multipart.mixed"; - } - - // Assign to Content-Type - msg.headers["content-type"] = contentType; - } - - // Assign to X-Mms-Message-Size - msg.headers["x-mms-message-size"] = messageSize; - - debug("msg: " + JSON.stringify(msg)); - - let istream = MMS.PduHelper.compose(null, msg); - if (!istream) { - debug("sendSendRequest: failed to compose M-Send.ind PDU"); - callback(MMS.MMS_PDU_ERROR_PERMANENT_FAILURE, null); - return; - } - - this.sendMmsRequest("POST", this.MMSC, istream, (function (status, data) { - if (status != HTTP_STATUS_OK) { - callback(MMS.MMS_PDU_ERROR_TRANSIENT_FAILURE, null); - } else if (!data) { - callback(MMS.MMS_PDU_ERROR_PERMANENT_FAILURE, null); - } else if (!this.parseStreamAndDispatch(data, {msg: msg, callback: callback})) { - callback(MMS.MMS_PDU_RESPONSE_ERROR_UNSUPPORTED_MESSAGE, null); - } - }).bind(this)); - }, - - /** - * @param data - * A wrapped object containing raw PDU data. - * @param options - * Additional options to be passed to corresponding PDU handler. - * - * @return true if incoming data parsed successfully and passed to PDU - * handler; false otherwise. - */ - parseStreamAndDispatch: function parseStreamAndDispatch(data, options) { - let msg = MMS.PduHelper.parse(data, null); - if (!msg) { - return false; - } - debug("parseStreamAndDispatch: msg = " + JSON.stringify(msg)); - - switch (msg.type) { - case MMS.MMS_PDU_TYPE_SEND_CONF: - this.handleSendConfirmation(msg, options); - break; - case MMS.MMS_PDU_TYPE_NOTIFICATION_IND: - this.handleNotificationIndication(msg, options); - break; - case MMS.MMS_PDU_TYPE_RETRIEVE_CONF: - this.handleRetrieveConfirmation(msg, options); - break; - case MMS.MMS_PDU_TYPE_DELIVERY_IND: - this.handleDeliveryIndication(msg, options); - break; - default: - debug("Unsupported X-MMS-Message-Type: " + msg.type); - return false; - } - - return true; - }, - - /** - * Handle incoming M-Send.conf PDU. - * - * @param msg - * The M-Send.conf message object. - */ - handleSendConfirmation: function handleSendConfirmation(msg, options) { - let status = msg.headers["x-mms-response-status"]; - if (status == null) { - return; - } - - if (status == MMS.MMS_PDU_ERROR_OK) { - // `This ID SHALL always be present after the MMS Proxy-Relay accepted - // the corresponding M-Send.req PDU. The ID enables a MMS Client to match - // delivery reports or read-report PDUs with previously sent MM.` - let messageId = msg.headers["message-id"]; - options.msg.headers["message-id"] = messageId; - } else if (this.isTransientError(status)) { - return; - } - - if (options.callback) { - options.callback(status, msg); - } + let transaction = new RetrieveTransaction(contentLocation); + transaction.run(callback); }, /** * Handle incoming M-Notification.ind PDU. * - * @param msg - * The MMS message object. + * @param notification + * The parsed MMS message object. */ - handleNotificationIndication: function handleNotificationIndication(msg) { - function callback(status, retr) { - if (this.isTransientError(status)) { + handleNotificationIndication: function handleNotificationIndication(notification) { + // TODO: bug 839436 - make DB be able to save MMS messages + // TODO: bug 810067 - support automatic/manual/never retrieval modes + + let url = notification.headers["x-mms-content-location"].uri; + // TODO: bug 810091 - don't download message twice on receiving duplicated + // notification + this.retrieveMessage(url, (function (mmsStatus, retrievedMsg) { + debug("retrievedMsg = " + JSON.stringify(retrievedMsg)); + if (this.isTransientError(mmsStatus)) { + // TODO: remove this check after bug 810097 is landed. return; } - let tid = msg.headers["x-mms-transaction-id"]; + let transactionId = notification.headers["x-mms-transaction-id"]; // For X-Mms-Report-Allowed - let wish = msg.headers["x-mms-delivery-report"]; + let wish = notification.headers["x-mms-delivery-report"]; // `The absence of the field does not indicate any default value.` // So we go checking the same field in retrieved message instead. - if ((wish == null) && retr) { - wish = retr.headers["x-mms-delivery-report"]; + if ((wish == null) && retrievedMsg) { + wish = retrievedMsg.headers["x-mms-delivery-report"]; } - let ra = this.getReportAllowed(this.confSendDeliveryReport, wish); + let reportAllowed = + this.getReportAllowed(this.confSendDeliveryReport, wish); - this.sendNotificationResponse(tid, status, ra); - } - - function retrCallback(error, retr) { - callback.call(this, MMS.translatePduErrorToStatus(error), retr); - } - - let url = msg.headers["x-mms-content-location"].uri; - this.sendMmsRequest("GET", url, null, (function (status, data) { - if (status != HTTP_STATUS_OK) { - callback.call(this, MMS.MMS_PDU_ERROR_TRANSIENT_FAILURE, null); - } else if (!data) { - callback.call(this, MMS.MMS_PDU_STATUS_DEFERRED, null); - } else if (!this.parseStreamAndDispatch(data, retrCallback.bind(this))) { - callback.call(this, MMS.MMS_PDU_STATUS_UNRECOGNISED, null); - } + let transaction = + new NotifyResponseTransaction(transactionId, mmsStatus, reportAllowed); + transaction.run(); }).bind(this)); }, /** - * Handle incoming M-Retrieve.conf PDU. + * Handle incoming M-Delivery.ind PDU. * * @param msg * The MMS message object. - * @param callback - * A callback function that accepts one argument as retrieved message. - */ - handleRetrieveConfirmation: function handleRetrieveConfirmation(msg, callback) { - function callbackIfValid(status, msg) { - if (callback) { - callback(status, msg) - } - } - - // Fix default header field values. - if (msg.headers["x-mms-delivery-report"] == null) { - msg.headers["x-mms-delivery-report"] = false; - } - - let status = msg.headers["x-mms-retrieve-status"]; - if ((status != null) && (status != MMS.MMS_PDU_ERROR_OK)) { - callbackIfValid(status, msg); - return; - } - // Todo: Please add code for inserting msg into database - // right here. - // see bug id: 811252 - B2G MMS: implement MMS database - }, - - /** - * Handle incoming M-Delivery.ind PDU. */ handleDeliveryIndication: function handleDeliveryIndication(msg) { + // TODO: bug 811252 - implement MMS database let messageId = msg.headers["message-id"]; debug("handleDeliveryIndication: got delivery report for " + messageId); }, - /** - * Update the MMS proxy info. - */ - updateMmsProxyInfo: function updateMmsProxyInfo() { - if (this.mmsProxy === null || this.mmsPort === null) { - debug("updateMmsProxyInfo: mmsProxy or mmsPort is not yet decided." ); - return; - } - - this.mmsProxyInfo = - gpps.newProxyInfo("http", - this.mmsProxy, - this.mmsPort, - Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST, - -1, null); - debug("updateMmsProxyInfo: " + JSON.stringify(this.mmsProxyInfo)); - }, - - /** - * Clear the MMS proxy settings. - */ - clearMmsProxySettings: function clearMmsProxySettings() { - this.mmsc = null; - this.mmsProxy = null; - this.mmsPort = null; - this.mmsProxyInfo = null; - }, - - /** - * @param status - * The MMS error type. - * - * @return true if it's a type of transient error; false otherwise. - */ - isTransientError: function isTransientError(status) { - return (status >= MMS.MMS_PDU_ERROR_TRANSIENT_FAILURE && - status < MMS.MMS_PDU_ERROR_PERMANENT_FAILURE); - }, - // nsIMmsService hasSupport: function hasSupport() { @@ -627,96 +806,25 @@ MmsService.prototype = { // nsIWapPushApplication receiveWapPush: function receiveWapPush(array, length, offset, options) { - this.parseStreamAndDispatch({array: array, offset: offset}); - }, + let data = {array: array, offset: offset}; + let msg = MMS.PduHelper.parse(data, null); + if (!msg) { + return false; + } + debug("receiveWapPush: msg = " + JSON.stringify(msg)); - // nsIObserver - - observe: function observe(subject, topic, data) { - switch (topic) { - case kNetworkInterfaceStateChangedTopic: { - this.mmsNetworkConnected = - gRIL.getDataCallStateByType("mms") == - Ci.nsINetworkInterface.NETWORK_STATE_CONNECTED; - - if (!this.mmsNetworkConnected) { - return; - } - - debug("Got the MMS network connected! Resend the buffered " + - "MMS requests: number: " + this.mmsRequestQueue.length); - this.timerToClearQueue.cancel(); - while (this.mmsRequestQueue.length) { - let mmsRequest = this.mmsRequestQueue.shift(); - this.sendMmsRequest(mmsRequest.method, - mmsRequest.url, - mmsRequest.istream, - mmsRequest.callback); - } + switch (msg.type) { + case MMS.MMS_PDU_TYPE_NOTIFICATION_IND: + this.handleNotificationIndication(msg); break; - } - case kXpcomShutdownObserverTopic: { - Services.obs.removeObserver(this, kXpcomShutdownObserverTopic); - Services.obs.removeObserver(this, kNetworkInterfaceStateChangedTopic); - this.mmsProxySettings.forEach(function(name) { - Services.prefs.removeObserver(name, this); - }, this); - this.timerToClearQueue.cancel(); - this.timerToClearQueueCb(); - this.timerToReleaseMmsConnection.cancel(); - this.timerToReleaseMmsConnectionCb(); + case MMS.MMS_PDU_TYPE_DELIVERY_IND: + this.handleDeliveryIndication(msg); break; - } - case kPrefenceChangedObserverTopic: { - try { - switch (data) { - case "ril.mms.mmsc": - this.mmsc = Services.prefs.getCharPref("ril.mms.mmsc"); - break; - case "ril.mms.mmsproxy": - this.mmsProxy = Services.prefs.getCharPref("ril.mms.mmsproxy"); - this.updateMmsProxyInfo(); - break; - case "ril.mms.mmsport": - this.mmsPort = Services.prefs.getIntPref("ril.mms.mmsport"); - this.updateMmsProxyInfo(); - break; - default: - break; - } - } catch (e) { - debug("Failed to update the MMS proxy settings from the preference."); - this.clearMmsProxySettings(); - } + default: + debug("Unsupported X-MMS-Message-Type: " + msg.type); break; - } } }, - - // nsIProtocolProxyFilter - - applyFilter: function applyFilter(service, uri, proxyInfo) { - if (!this.mmsNetworkConnected) { - debug("applyFilter: the MMS network is not connected."); - return proxyInfo; - } - - if (this.mmsc === null || uri.prePath != this.mmsc) { - debug("applyFilter: MMSC is not matched."); - return proxyInfo; - } - - if (this.mmsProxyInfo === null) { - debug("applyFilter: MMS proxy info is not yet decided."); - return proxyInfo; - } - - // Fall-through, reutrn the MMS proxy info. - debug("applyFilter: MMSC is matched: " + - JSON.stringify({ mmsc: this.mmsc, - mmsProxyInfo: this.mmsProxyInfo })); - return this.mmsProxyInfo; - } }; this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MmsService]); @@ -729,4 +837,3 @@ if (DEBUG) { } else { debug = function (s) {}; } - diff --git a/dom/mms/src/ril/MmsService.manifest b/dom/mms/src/ril/MmsService.manifest index 0ff07bea7983..1fc736e1c2f8 100644 --- a/dom/mms/src/ril/MmsService.manifest +++ b/dom/mms/src/ril/MmsService.manifest @@ -1,4 +1,3 @@ # MmsService.js component {217ddd76-75db-4210-955d-8806cd8d87f9} MmsService.js contract @mozilla.org/mms/rilmmsservice;1 {217ddd76-75db-4210-955d-8806cd8d87f9} -category profile-after-change MmsService @mozilla.org/mms/rilmmsservice;1 diff --git a/dom/mms/src/ril/WspPduHelper.jsm b/dom/mms/src/ril/WspPduHelper.jsm index 0c3ad2b885c1..51c32b9e3272 100644 --- a/dom/mms/src/ril/WspPduHelper.jsm +++ b/dom/mms/src/ril/WspPduHelper.jsm @@ -2154,7 +2154,10 @@ this.PduHelper = { let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"] .createInstance(Ci.nsIScriptableUnicodeConverter); - let entry = WSP_WELL_KNOWN_CHARSETS[charset]; + let entry; + if (charset) { + entry = WSP_WELL_KNOWN_CHARSETS[charset]; + } // Set converter to default one if (entry && entry.converter) is null. // @see OMA-TS-MMS-CONF-V1_3-20050526-D 7.1.9 conv.charset = (entry && entry.converter) || "UTF-8"; @@ -2164,6 +2167,33 @@ this.PduHelper = { } return null; }, + + /** + * @param strContent + * Decoded string content. + * @param charset + * Charset for encode. + * + * @return An encoded UInt8Array of string content. + */ + encodeStringContent: function encodeStringContent(strContent, charset) { + let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + + let entry; + if (charset) { + entry = WSP_WELL_KNOWN_CHARSETS[charset]; + } + // Set converter to default one if (entry && entry.converter) is null. + // @see OMA-TS-MMS-CONF-V1_3-20050526-D 7.1.9 + conv.charset = (entry && entry.converter) || "UTF-8"; + try { + return conv.convertToByteArray(strContent); + } catch (e) { + } + return null; + }, + /** * Parse multiple header fields with end mark. * @@ -2394,7 +2424,19 @@ this.PduHelper = { // Encode headersLen, DataLen let headersLen = data.offset; UintVar.encode(data, headersLen); - UintVar.encode(data, part.content.length); + if (typeof part.content === "string") { + let charset; + if (contentType && contentType.params && contentType.params.charset && + contentType.params.charset.charset) { + charset = contentType.params.charset.charset; + } + part.content = this.encodeStringContent(part.content, charset); + UintVar.encode(data, part.content.length); + } else if (part.content instanceof Uint8Array) { + UintVar.encode(data, part.content.length); + } else { + throw new TypeError(); + } // Move them to the beginning of encoded octet array. let slice1 = data.array.slice(headersLen);