gecko-dev/dom/mobilemessage/gonk/MmsService.js

2737 строки
95 KiB
JavaScript
Исходник Ответственный История

Этот файл содержит неоднозначные символы Юникода!

Этот файл содержит неоднозначные символы Юникода, которые могут быть перепутаны с другими в текущей локали. Если это намеренно, можете спокойно проигнорировать это предупреждение. Используйте кнопку Экранировать, чтобы подсветить эти символы.

/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.importGlobalProperties(['Blob', 'FileReader']);
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PhoneNumberUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
const GONK_MMSSERVICE_CONTRACTID = "@mozilla.org/mms/gonkmmsservice;1";
const GONK_MMSSERVICE_CID = Components.ID("{9b069b8c-8697-11e4-a406-474f5190272b}");
var DEBUG = false;
function debug(s) {
dump("-@- MmsService: " + s + "\n");
};
const kSmsSendingObserverTopic = "sms-sending";
const kSmsSentObserverTopic = "sms-sent";
const kSmsFailedObserverTopic = "sms-failed";
const kSmsReceivedObserverTopic = "sms-received";
const kSmsRetrievingObserverTopic = "sms-retrieving";
const kSmsDeliverySuccessObserverTopic = "sms-delivery-success";
const kSmsDeliveryErrorObserverTopic = "sms-delivery-error";
const kSmsReadSuccessObserverTopic = "sms-read-success";
const kSmsReadErrorObserverTopic = "sms-read-error";
const kSmsDeletedObserverTopic = "sms-deleted";
const NS_XPCOM_SHUTDOWN_OBSERVER_ID = "xpcom-shutdown";
const kNetworkConnStateChangedTopic = "network-connection-state-changed";
const kPrefMmsDebuggingEnabled = "mms.debugging.enabled";
// HTTP status codes:
// @see http://tools.ietf.org/html/rfc2616#page-39
const HTTP_STATUS_OK = 200;
// Non-standard HTTP status for internal use.
const _HTTP_STATUS_ACQUIRE_CONNECTION_SUCCESS = 0;
const _HTTP_STATUS_USER_CANCELLED = -1;
const _HTTP_STATUS_RADIO_DISABLED = -2;
const _HTTP_STATUS_NO_SIM_CARD = -3;
const _HTTP_STATUS_ACQUIRE_TIMEOUT = -4;
const _HTTP_STATUS_FAILED_TO_ROUTE = -5;
// Non-standard MMS status for internal use.
const _MMS_ERROR_MESSAGE_DELETED = -1;
const _MMS_ERROR_RADIO_DISABLED = -2;
const _MMS_ERROR_NO_SIM_CARD = -3;
const _MMS_ERROR_SIM_CARD_CHANGED = -4;
const _MMS_ERROR_SHUTDOWN = -5;
const _MMS_ERROR_USER_CANCELLED_NO_REASON = -6;
const _MMS_ERROR_SIM_NOT_MATCHED = -7;
const _MMS_ERROR_FAILED_TO_ROUTE = -8;
const CONFIG_SEND_REPORT_NEVER = 0;
const CONFIG_SEND_REPORT_DEFAULT_NO = 1;
const CONFIG_SEND_REPORT_DEFAULT_YES = 2;
const CONFIG_SEND_REPORT_ALWAYS = 3;
const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
const TIME_TO_BUFFER_MMS_REQUESTS = 30000;
const PREF_TIME_TO_RELEASE_MMS_CONNECTION =
Services.prefs.getIntPref("network.gonk.ms-release-mms-connection");
const kPrefRetrievalMode = 'dom.mms.retrieval_mode';
const RETRIEVAL_MODE_MANUAL = "manual";
const RETRIEVAL_MODE_AUTOMATIC = "automatic";
const RETRIEVAL_MODE_AUTOMATIC_HOME = "automatic-home";
const RETRIEVAL_MODE_NEVER = "never";
//Internal const values.
const DELIVERY_RECEIVED = "received";
const DELIVERY_NOT_DOWNLOADED = "not-downloaded";
const DELIVERY_SENDING = "sending";
const DELIVERY_SENT = "sent";
const DELIVERY_ERROR = "error";
const DELIVERY_STATUS_SUCCESS = "success";
const DELIVERY_STATUS_PENDING = "pending";
const DELIVERY_STATUS_ERROR = "error";
const DELIVERY_STATUS_REJECTED = "rejected";
const DELIVERY_STATUS_MANUAL = "manual";
const DELIVERY_STATUS_NOT_APPLICABLE = "not-applicable";
const PREF_SEND_RETRY_COUNT =
Services.prefs.getIntPref("dom.mms.sendRetryCount");
const PREF_SEND_RETRY_INTERVAL = (function () {
let intervals =
Services.prefs.getCharPref("dom.mms.sendRetryInterval").split(",");
for (let i = 0; i < PREF_SEND_RETRY_COUNT; ++i) {
intervals[i] = parseInt(intervals[i], 10);
// If one of the intervals isn't valid (e.g., 0 or NaN),
// assign a 1-minute interval to it as a default.
if (!intervals[i]) {
intervals[i] = 60000;
}
}
intervals.length = PREF_SEND_RETRY_COUNT;
return intervals;
})();
const PREF_RETRIEVAL_RETRY_COUNT =
Services.prefs.getIntPref("dom.mms.retrievalRetryCount");
const PREF_RETRIEVAL_RETRY_INTERVALS = (function() {
let intervals =
Services.prefs.getCharPref("dom.mms.retrievalRetryIntervals").split(",");
for (let i = 0; i < PREF_RETRIEVAL_RETRY_COUNT; ++i) {
intervals[i] = parseInt(intervals[i], 10);
// If one of the intervals isn't valid (e.g., 0 or NaN),
// assign a 10-minute interval to it as a default.
if (!intervals[i]) {
intervals[i] = 600000;
}
}
intervals.length = PREF_RETRIEVAL_RETRY_COUNT;
return intervals;
})();
const kPrefRilNumRadioInterfaces = "ril.numRadioInterfaces";
const kPrefDefaultServiceId = "dom.mms.defaultServiceId";
XPCOMUtils.defineLazyServiceGetter(this, "gpps",
"@mozilla.org/network/protocol-proxy-service;1",
"nsIProtocolProxyService");
XPCOMUtils.defineLazyServiceGetter(this, "gIccService",
"@mozilla.org/icc/iccservice;1",
"nsIIccService");
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
XPCOMUtils.defineLazyServiceGetter(this, "gMobileMessageDatabaseService",
"@mozilla.org/mobilemessage/gonkmobilemessagedatabaseservice;1",
"nsIGonkMobileMessageDatabaseService");
XPCOMUtils.defineLazyServiceGetter(this, "gMobileMessageService",
"@mozilla.org/mobilemessage/mobilemessageservice;1",
"nsIMobileMessageService");
XPCOMUtils.defineLazyServiceGetter(this, "gSystemMessenger",
"@mozilla.org/system-message-internal;1",
"nsISystemMessagesInternal");
XPCOMUtils.defineLazyServiceGetter(this, "gRil",
"@mozilla.org/ril;1",
"nsIRadioInterfaceLayer");
XPCOMUtils.defineLazyServiceGetter(this, "gNetworkManager",
"@mozilla.org/network/manager;1",
"nsINetworkManager");
XPCOMUtils.defineLazyServiceGetter(this, "gMobileConnectionService",
"@mozilla.org/mobileconnection/mobileconnectionservice;1",
"nsIMobileConnectionService");
XPCOMUtils.defineLazyServiceGetter(this, "gNetworkService",
"@mozilla.org/network/service;1",
"nsINetworkService");
XPCOMUtils.defineLazyGetter(this, "MMS", function() {
let MMS = {};
Cu.import("resource://gre/modules/MmsPduHelper.jsm", MMS);
return MMS;
});
// Internal Utilities
/**
* Return default service Id for MMS.
*/
function getDefaultServiceId() {
let id = Services.prefs.getIntPref(kPrefDefaultServiceId);
let numRil = Services.prefs.getIntPref(kPrefRilNumRadioInterfaces);
if (id >= numRil || id < 0) {
id = 0;
}
return id;
}
/**
* Return radio disabled state.
*/
function isRadioOff(aServiceId) {
let connection = gMobileConnectionService.getItemByServiceId(aServiceId);
return connection.radioState
!== Ci.nsIMobileConnection.MOBILE_RADIO_STATE_ENABLED;
}
/**
* Helper Class to control MMS Data Connection.
*/
function MmsConnection(aServiceId) {
this.serviceId = aServiceId;
this.radioInterface = gRil.getRadioInterface(aServiceId);
this.pendingCallbacks = [];
this.hostsToRoute = [];
this.connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this.disconnectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
};
MmsConnection.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
/** MMS proxy settings. */
mmsc: "",
mmsProxy: "",
mmsPort: -1,
setApnSetting: function(networkInfo) {
this.mmsc = networkInfo.mmsc;
this.mmsProxy = networkInfo.mmsProxy;
this.mmsPort = networkInfo.mmsPort;
},
get proxyInfo() {
if (!this.mmsProxy) {
if (DEBUG) debug("getProxyInfo: MMS proxy is not available.");
return null;
}
let port = this.mmsPort;
if (port <= 0) {
port = 80;
if (DEBUG) debug("getProxyInfo: port is not valid. Set to defult (80).");
}
let proxyInfo =
gpps.newProxyInfo("http", this.mmsProxy, port,
Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST,
-1, null);
if (DEBUG) debug("getProxyInfo: " + JSON.stringify(proxyInfo));
return proxyInfo;
},
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: null,
/** MMS network connection reference count. */
refCount: 0,
// cache of hosts to be accessed when this connection is alive.
hostsToRoute: null,
// cache of the networkInfo acquired during this connection.
networkInfo: null,
connectTimer: null,
disconnectTimer: null,
/**
* Callback when |connectTimer| is timeout or cancelled by shutdown.
*/
flushPendingCallbacks: function(status) {
if (DEBUG) debug("flushPendingCallbacks: " + this.pendingCallbacks.length
+ " pending callbacks with status: " + status);
while (this.pendingCallbacks.length) {
let callback = this.pendingCallbacks.shift();
let connected = (status == _HTTP_STATUS_ACQUIRE_CONNECTION_SUCCESS);
callback(connected, status);
}
},
/**
* Callback when |disconnectTimer| is timeout or cancelled by shutdown.
*/
onDisconnectTimerTimeout: function() {
if (DEBUG) debug("onDisconnectTimerTimeout: deactivate the MMS data call.");
if (!this.connected) {
return;
}
let deactivateMmsDataCall = (aError) => {
if (aError) debug("Failed to removeHostRoute: " + aError);
// Clear cache.
this.hostsToRoute = [];
this.networkInfo = null;
this.radioInterface.deactivateDataCallByType(Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_MMS);
};
let promises =
this.hostsToRoute.map((aHost) => {
return gNetworkManager.removeHostRoute(this.networkInfo, aHost);
});
return Promise.all(promises)
.then(() => deactivateMmsDataCall(),
(aError) => deactivateMmsDataCall(aError));
},
init: function() {
Services.obs.addObserver(this, kNetworkConnStateChangedTopic,
false);
Services.obs.addObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
},
/**
* Return the roaming status of voice call.
*
* @return true if voice call is roaming.
*/
isVoiceRoaming: function() {
let connection =
gMobileConnectionService.getItemByServiceId(this.serviceId);
let isRoaming = connection && connection.voice && connection.voice.roaming;
if (DEBUG) debug("isVoiceRoaming = " + isRoaming);
return isRoaming;
},
/**
* Get phone number from iccInfo.
*
* If the icc card is gsm card, the phone number is in msisdn.
* @see nsIGsmIccInfo
*
* Otherwise, the phone number is in mdn.
* @see nsICdmaIccInfo
*/
getPhoneNumber: function() {
let number;
// Get the proper IccInfo based on the current card type.
try {
let iccInfo = null;
let baseIccInfo = this.getIccInfo();
if (baseIccInfo.iccType === 'ruim' || baseIccInfo.iccType === 'csim') {
iccInfo = baseIccInfo.QueryInterface(Ci.nsICdmaIccInfo);
number = iccInfo.mdn;
} else {
iccInfo = baseIccInfo.QueryInterface(Ci.nsIGsmIccInfo);
number = iccInfo.msisdn;
}
} catch (e) {
if (DEBUG) {
debug("Exception - QueryInterface failed on iccinfo for GSM/CDMA info");
}
return null;
}
return number;
},
/**
* A utility function to get IccInfo of the SIM card (if installed).
*/
getIccInfo: function() {
let icc = gIccService.getIccByServiceId(this.serviceId);
return icc ? icc.iccInfo : null;
},
/**
* A utility function to get CardState of the SIM card (if installed).
*/
getCardState: function() {
let icc = gIccService.getIccByServiceId(this.serviceId);
return icc ? icc.cardState : Ci.nsIIcc.CARD_STATE_UNKNOWN;
},
/**
* A utility function to get the ICC ID of the SIM card (if installed).
*/
getIccId: function() {
let iccInfo = this.getIccInfo();
if (!iccInfo) {
return null;
}
return iccInfo.iccid;
},
/**
* Acquire the MMS network connection.
*
* @param callback
* Callback function when either the connection setup is done,
* timeout, or failed. Parameters are:
* - A boolean value indicates whether the connection is ready.
* - Acquire connection status: _HTTP_STATUS_ACQUIRE_*.
*
* @return true if the callback for MMS network connection is done; false
* otherwise.
*/
acquire: function(callback) {
this.refCount++;
this.connectTimer.cancel();
this.disconnectTimer.cancel();
// If the MMS network is not yet connected, buffer the
// MMS request and try to setup the MMS network first.
if (!this.connected) {
this.pendingCallbacks.push(callback);
let errorStatus;
if (isRadioOff(this.serviceId)) {
if (DEBUG) debug("Error! Radio is disabled when sending MMS.");
errorStatus = _HTTP_STATUS_RADIO_DISABLED;
} else if (this.getCardState() != Ci.nsIIcc.CARD_STATE_READY) {
if (DEBUG) debug("Error! SIM card is not ready when sending MMS.");
errorStatus = _HTTP_STATUS_NO_SIM_CARD;
}
if (errorStatus != null) {
this.flushPendingCallbacks(errorStatus);
return true;
}
// 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.flushPendingCallbacks(_HTTP_STATUS_ACQUIRE_TIMEOUT),
TIME_TO_BUFFER_MMS_REQUESTS,
Ci.nsITimer.TYPE_ONE_SHOT);
// Bug 1059110: Ensure all the initialization are done before setup data call.
if (DEBUG) debug("acquire: buffer the MMS request and setup the MMS data call.");
this.radioInterface.setupDataCallByType(Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_MMS);
return false;
}
callback(true, _HTTP_STATUS_ACQUIRE_CONNECTION_SUCCESS);
return true;
},
/**
* Release the MMS network connection.
*/
release: function() {
this.refCount--;
if (this.refCount <= 0) {
this.refCount = 0;
// The waiting is too small, just skip the timer creation.
if (PREF_TIME_TO_RELEASE_MMS_CONNECTION < 1000) {
this.onDisconnectTimerTimeout();
return;
}
// 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(),
PREF_TIME_TO_RELEASE_MMS_CONNECTION,
Ci.nsITimer.TYPE_ONE_SHOT);
}
},
/**
* Helper to ensure the routing of each transaction.
*
* @param url
* Optional url for retrieving mms.
*
* @return a Promise resolved if added or rejected, otherwise.
*/
ensureRouting: function(url) {
let host = this.mmsProxy;
if (!this.mmsProxy) {
host = url;
}
try {
let uri = Services.io.newURI(host, null, null);
host = uri.host;
} catch (e) {}
return gNetworkManager.addHostRoute(this.networkInfo, host)
.then(() => {
if (this.hostsToRoute.indexOf(host) < 0) {
this.hostsToRoute.push(host);
}
});
},
shutdown: function() {
Services.obs.removeObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
Services.obs.removeObserver(this, kNetworkConnStateChangedTopic);
this.connectTimer.cancel();
this.flushPendingCallbacks(_HTTP_STATUS_RADIO_DISABLED);
this.disconnectTimer.cancel();
this.onDisconnectTimerTimeout();
},
// nsIObserver
observe: function(subject, topic, data) {
switch (topic) {
case kNetworkConnStateChangedTopic: {
// The network info for MMS connection must be nsIRilNetworkInfo.
if (!(subject instanceof Ci.nsIRilNetworkInfo)) {
return;
}
// Check if the network state change belongs to this service.
let networkInfo = subject.QueryInterface(Ci.nsIRilNetworkInfo);
if (networkInfo.serviceId != this.serviceId ||
networkInfo.type != Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_MMS) {
return;
}
let connected =
networkInfo.state == Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED;
// Return if the MMS network state doesn't change, where the network
// state change can come from other non-MMS networks.
if (connected == this.connected) {
return;
}
this.connected = connected;
if (!this.connected) {
this.hostsToRoute = [];
this.networkInfo = null;
return;
}
// Set up the MMS APN setting based on the connected MMS network,
// which is going to be used for the HTTP requests later.
this.setApnSetting(networkInfo);
// Cache connected network info.
this.networkInfo = networkInfo;
if (DEBUG) debug("Got the MMS network connected! Resend the buffered " +
"MMS requests: number: " + this.pendingCallbacks.length);
this.connectTimer.cancel();
this.flushPendingCallbacks(_HTTP_STATUS_ACQUIRE_CONNECTION_SUCCESS);
break;
}
case NS_XPCOM_SHUTDOWN_OBSERVER_ID: {
this.shutdown();
}
}
}
};
XPCOMUtils.defineLazyGetter(this, "gMmsConnections", function() {
return {
_connections: null,
getConnByServiceId: function(id) {
if (!this._connections) {
this._connections = [];
}
let conn = this._connections[id];
if (conn) {
return conn;
}
conn = this._connections[id] = new MmsConnection(id);
conn.init();
return conn;
},
getConnByIccId: function(aIccId) {
if (!aIccId) {
// If the ICC ID isn't available, it means the MMS has been received
// during the previous version that didn't take the DSDS scenario
// into consideration. Tentatively, get connection from serviceId(0) by
// default is better than nothing. Although it might use the wrong
// SIM to download the desired MMS, eventually it would still fail to
// download due to the wrong MMSC and proxy settings.
return this.getConnByServiceId(0);
}
let numCardAbsent = 0;
let numRadioInterfaces = gRil.numRadioInterfaces;
for (let clientId = 0; clientId < numRadioInterfaces; clientId++) {
let mmsConnection = this.getConnByServiceId(clientId);
let iccId = mmsConnection.getIccId();
if (iccId === null) {
numCardAbsent++;
continue;
}
if (iccId === aIccId) {
return mmsConnection;
}
}
throw ((numCardAbsent === numRadioInterfaces)?
_MMS_ERROR_NO_SIM_CARD: _MMS_ERROR_SIM_NOT_MATCHED);
},
};
});
/**
* Implementation of nsIProtocolProxyFilter for MMS Proxy
*/
function MmsProxyFilter(mmsConnection, url) {
this.mmsConnection = mmsConnection;
this.uri = Services.io.newURI(url, null, null);
}
MmsProxyFilter.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolProxyFilter]),
// nsIProtocolProxyFilter
applyFilter: function(proxyService, uri, proxyInfo) {
if (!this.uri.equals(uri)) {
if (DEBUG) debug("applyFilter: content uri = " + JSON.stringify(this.uri) +
" is not matched with uri = " + JSON.stringify(uri) + " .");
return proxyInfo;
}
// Fall-through, reutrn the MMS proxy info.
let mmsProxyInfo = this.mmsConnection.proxyInfo;
if (DEBUG) {
debug("applyFilter: MMSC/Content Location is matched with: " +
JSON.stringify({ uri: JSON.stringify(this.uri),
mmsProxyInfo: mmsProxyInfo }));
}
return mmsProxyInfo ? mmsProxyInfo : proxyInfo;
}
};
XPCOMUtils.defineLazyGetter(this, "gMmsTransactionHelper", function() {
let helper = {
/**
* Send MMS request to MMSC.
*
* @param mmsConnection
* The MMS connection.
* @param method
* "GET" or "POST".
* @param url
* Target url string or null to be replaced by mmsc url.
* @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(mmsConnection, method, url, istream, callback) {
// TODO: bug 810226 - Support GPRS bearer for MMS transmission and reception.
let cancellable = {
callback: callback,
isDone: false,
isCancelled: false,
cancel: function() {
if (this.isDone) {
// It's too late to cancel.
return;
}
this.isCancelled = true;
if (this.isAcquiringConn) {
// We cannot cancel data connection setup here, so we invoke done()
// here and handle |cancellable.isDone| in callback function of
// |mmsConnection.acquire|.
this.done(_HTTP_STATUS_USER_CANCELLED, null);
} else if (this.xhr) {
// Client has already sent the HTTP request. Try to abort it.
this.xhr.abort();
}
},
done: function(httpStatus, data) {
this.isDone = true;
if (!this.callback) {
return;
}
if (this.isCancelled) {
this.callback(_HTTP_STATUS_USER_CANCELLED, null);
} else {
this.callback(httpStatus, data);
}
}
};
cancellable.isAcquiringConn =
!mmsConnection.acquire((connected, errorCode) => {
cancellable.isAcquiringConn = false;
if (!connected || cancellable.isCancelled) {
mmsConnection.release();
if (!cancellable.isDone) {
cancellable.done(cancellable.isCancelled ?
_HTTP_STATUS_USER_CANCELLED : errorCode, null);
}
return;
}
// MMSC is available after an MMS connection is successfully acquired.
if (!url) {
url = mmsConnection.mmsc;
}
let startTransaction = netId => {
if (DEBUG) debug("sendRequest: register proxy filter to " + url);
let proxyFilter = new MmsProxyFilter(mmsConnection, url);
gpps.registerFilter(proxyFilter, 0);
cancellable.xhr =
this.sendHttpRequest(mmsConnection, method,
url, istream, proxyFilter, netId,
(aHttpStatus, aData) =>
cancellable.done(aHttpStatus, aData));
};
let onRejected = aReason => {
debug('Failed to start a transaction: ' + aReason);
mmsConnection.release();
cancellable.done(_HTTP_STATUS_FAILED_TO_ROUTE, null);
};
// TODO: |getNetId| will be implemented as a sync call in nsINetworkManager
// once Bug 1141903 is landed.
mmsConnection.ensureRouting(url)
.then(() => gNetworkService.getNetId(mmsConnection.networkInfo.name))
.then((netId) => startTransaction(netId))
.catch((aReason) => onRejected(aReason));
});
return cancellable;
},
sendHttpRequest: function(mmsConnection, method, url, istream, proxyFilter,
netId, callback) {
let releaseMmsConnectionAndCallback = (httpStatus, data) => {
gpps.unregisterFilter(proxyFilter);
// Always release the MMS network connection before callback.
mmsConnection.release();
callback(httpStatus, data);
};
try {
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
// Basic setups
xhr.networkInterfaceId = netId;
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());
}
// 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.onreadystatechange = () => {
if (xhr.readyState != Ci.nsIXMLHttpRequest.DONE) {
return;
}
let data = null;
switch (xhr.status) {
case HTTP_STATUS_OK: {
if (DEBUG) 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);
if (DEBUG) debug("res: " + JSON.stringify(partial));
}
}
data = {array: array, offset: 0};
break;
}
default: {
if (DEBUG) debug("xhr done, but status = " + xhr.status +
", statusText = " + xhr.statusText);
break;
}
}
releaseMmsConnectionAndCallback(xhr.status, data);
};
// Send request
xhr.send(istream);
return xhr;
} catch (e) {
if (DEBUG) debug("xhr error, can't send: " + e.message);
releaseMmsConnectionAndCallback(0, null);
return null;
}
},
/**
* Count number of recipients(to, cc, bcc fields).
*
* @param recipients
* The recipients in MMS message object.
* @return the number of recipients
* @see OMA-TS-MMS_CONF-V1_3-20110511-C section 10.2.5
*/
countRecipients: function(recipients) {
if (recipients && recipients.address) {
return 1;
}
let totalRecipients = 0;
if (!Array.isArray(recipients)) {
return 0;
}
totalRecipients += recipients.length;
for (let ix = 0; ix < recipients.length; ++ix) {
if (recipients[ix].address.length > MMS.MMS_MAX_LENGTH_RECIPIENT) {
throw new Error("MMS_MAX_LENGTH_RECIPIENT error");
}
if (recipients[ix].type === "email") {
let found = recipients[ix].address.indexOf("<");
let lenMailbox = recipients[ix].address.length - found;
if(lenMailbox > MMS.MMS_MAX_LENGTH_MAILBOX_PORTION) {
throw new Error("MMS_MAX_LENGTH_MAILBOX_PORTION error");
}
}
}
return totalRecipients;
},
/**
* Check maximum values of MMS parameters.
*
* @param msg
* The MMS message object.
* @return true if the lengths are less than the maximum values of MMS
* parameters.
* @see OMA-TS-MMS_CONF-V1_3-20110511-C section 10.2.5
*/
checkMaxValuesParameters: function(msg) {
let subject = msg.headers["subject"];
if (subject && subject.length > MMS.MMS_MAX_LENGTH_SUBJECT) {
return false;
}
let totalRecipients = 0;
try {
totalRecipients += this.countRecipients(msg.headers["to"]);
totalRecipients += this.countRecipients(msg.headers["cc"]);
totalRecipients += this.countRecipients(msg.headers["bcc"]);
} catch (ex) {
if (DEBUG) debug("Exception caught : " + ex);
return false;
}
if (totalRecipients < 1 ||
totalRecipients > MMS.MMS_MAX_TOTAL_RECIPIENTS) {
return false;
}
if (!Array.isArray(msg.parts)) {
return true;
}
for (let i = 0; i < msg.parts.length; i++) {
if (msg.parts[i].headers["content-type"] &&
msg.parts[i].headers["content-type"].params) {
let name = msg.parts[i].headers["content-type"].params["name"];
if (name && name.length > MMS.MMS_MAX_LENGTH_NAME_CONTENT_TYPE) {
return false;
}
}
}
return true;
},
translateHttpStatusToMmsStatus: function(httpStatus, cancelledReason,
defaultStatus) {
switch(httpStatus) {
case _HTTP_STATUS_USER_CANCELLED:
return cancelledReason;
case _HTTP_STATUS_RADIO_DISABLED:
return _MMS_ERROR_RADIO_DISABLED;
case _HTTP_STATUS_NO_SIM_CARD:
return _MMS_ERROR_NO_SIM_CARD;
case _HTTP_STATUS_FAILED_TO_ROUTE:
return _MMS_ERROR_FAILED_TO_ROUTE;
case HTTP_STATUS_OK:
return MMS.MMS_PDU_ERROR_OK;
default:
return defaultStatus;
}
}
};
return helper;
});
/**
* Send M-NotifyResp.ind back to MMSC.
*
* @param mmsConnection
* The MMS connection.
* @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(mmsConnection, transactionId, status,
reportAllowed) {
this.mmsConnection = mmsConnection;
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(callback) {
let requestCallback;
if (callback) {
requestCallback = (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(this.mmsConnection,
"POST",
null,
this.istream,
requestCallback);
}
};
/**
* CancellableTransaction - base class inherited by [Send|Retrieve]Transaction.
* We can call |cancelRunning(reason)| to cancel the on-going transaction.
* @param cancellableId
* An ID used to keep track of if an message is deleted from DB.
* @param serviceId
* An ID used to keep track of if the primary SIM service is changed.
*/
function CancellableTransaction(cancellableId, serviceId) {
this.cancellableId = cancellableId;
this.serviceId = serviceId;
this.isCancelled = false;
}
CancellableTransaction.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsIMobileConnectionListener]),
// The timer for retrying sending or retrieving process.
timer: null,
// Keep a reference to the callback when calling
// |[Send|Retrieve]Transaction.run(callback)|.
runCallback: null,
isObserversAdded: false,
cancelledReason: _MMS_ERROR_USER_CANCELLED_NO_REASON,
registerRunCallback: function(callback) {
if (!this.isObserversAdded) {
Services.obs.addObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
Services.obs.addObserver(this, kSmsDeletedObserverTopic, false);
Services.prefs.addObserver(kPrefDefaultServiceId, this, false);
gMobileConnectionService
.getItemByServiceId(this.serviceId).registerListener(this);
this.isObserversAdded = true;
}
this.runCallback = callback;
this.isCancelled = false;
},
removeObservers: function() {
if (this.isObserversAdded) {
Services.obs.removeObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
Services.obs.removeObserver(this, kSmsDeletedObserverTopic);
Services.prefs.removeObserver(kPrefDefaultServiceId, this);
gMobileConnectionService
.getItemByServiceId(this.serviceId).unregisterListener(this);
this.isObserversAdded = false;
}
},
runCallbackIfValid: function(mmsStatus, msg) {
this.removeObservers();
if (this.runCallback) {
this.runCallback(mmsStatus, msg);
this.runCallback = null;
}
},
// Keep a reference to the cancellable when calling
// |gMmsTransactionHelper.sendRequest(...)|.
cancellable: null,
cancelRunning: function(reason) {
this.isCancelled = true;
this.cancelledReason = reason;
if (this.timer) {
// The sending or retrieving process is waiting for the next retry.
// What we only need to do is to cancel the timer.
this.timer.cancel();
this.timer = null;
this.runCallbackIfValid(reason, null);
return;
}
if (this.cancellable) {
// The sending or retrieving process is still running. We attempt to
// abort the HTTP request.
this.cancellable.cancel();
this.cancellable = null;
}
},
// nsIObserver
observe: function(subject, topic, data) {
switch (topic) {
case NS_XPCOM_SHUTDOWN_OBSERVER_ID: {
this.cancelRunning(_MMS_ERROR_SHUTDOWN);
break;
}
case kSmsDeletedObserverTopic: {
let deletedInfo = subject.QueryInterface(Ci.nsIDeletedMessageInfo);
if (deletedInfo && deletedInfo.deletedMessageIds &&
deletedInfo.deletedMessageIds.indexOf(this.cancellableId) >= 0) {
this.cancelRunning(_MMS_ERROR_MESSAGE_DELETED);
}
break;
}
case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID: {
if (data === kPrefDefaultServiceId &&
this.serviceId != getDefaultServiceId()) {
this.cancelRunning(_MMS_ERROR_SIM_CARD_CHANGED);
}
break;
}
}
},
// nsIMobileConnectionListener
notifyVoiceChanged: function() {},
notifyDataChanged: function() {},
notifyDataError: function(message) {},
notifyCFStateChanged: function(action, reason, number, timeSeconds, serviceClass) {},
notifyEmergencyCbModeChanged: function(active, timeoutMs) {},
notifyOtaStatusChanged: function(status) {},
notifyRadioStateChanged: function() {
if (isRadioOff(this.serviceId)) {
this.cancelRunning(_MMS_ERROR_RADIO_DISABLED);
}
},
notifyClirModeChanged: function(mode) {},
notifyLastKnownNetworkChanged: function() {},
notifyLastKnownHomeNetworkChanged: function() {},
notifyNetworkSelectionModeChanged: function() {},
notifyDeviceIdentitiesChanged: function() {}
};
/**
* Class for retrieving message from MMSC, which inherits CancellableTransaction.
*
* @param contentLocation
* X-Mms-Content-Location of the message.
*/
function RetrieveTransaction(mmsConnection, cancellableId, contentLocation) {
this.mmsConnection = mmsConnection;
// Call |CancellableTransaction| constructor.
CancellableTransaction.call(this, cancellableId, mmsConnection.serviceId);
this.contentLocation = contentLocation;
}
RetrieveTransaction.prototype = Object.create(CancellableTransaction.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: {
value: function(callback) {
this.registerRunCallback(callback);
this.retryCount = 0;
let retryCallback = (mmsStatus, msg) => {
if (MMS.MMS_PDU_STATUS_DEFERRED == mmsStatus &&
this.retryCount < PREF_RETRIEVAL_RETRY_COUNT) {
let time = PREF_RETRIEVAL_RETRY_INTERVALS[this.retryCount];
if (DEBUG) debug("Fail to retrieve. Will retry after: " + time);
if (this.timer == null) {
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
}
this.timer.initWithCallback(() => this.retrieve(retryCallback),
time, Ci.nsITimer.TYPE_ONE_SHOT);
this.retryCount++;
return;
}
this.runCallbackIfValid(mmsStatus, msg);
};
this.retrieve(retryCallback);
},
enumerable: true,
configurable: true,
writable: true
},
/**
* @param callback
* A callback function that takes two arguments: one for X-Mms-Status,
* the other for the parsed M-Retrieve.conf message.
*/
retrieve: {
value: function(callback) {
this.timer = null;
this.cancellable =
gMmsTransactionHelper.sendRequest(this.mmsConnection,
"GET", this.contentLocation, null,
(httpStatus, data) => {
let mmsStatus = gMmsTransactionHelper
.translateHttpStatusToMmsStatus(httpStatus,
this.cancelledReason,
MMS.MMS_PDU_STATUS_DEFERRED);
if (mmsStatus != MMS.MMS_PDU_ERROR_OK) {
callback(mmsStatus, null);
return;
}
if (!data) {
callback(MMS.MMS_PDU_STATUS_DEFERRED, null);
return;
}
let retrieved = MMS.PduHelper.parse(data, null);
if (!retrieved || (retrieved.type != MMS.MMS_PDU_TYPE_RETRIEVE_CONF)) {
callback(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)) {
callback(MMS.translatePduErrorToStatus(retrieveStatus), retrieved);
return;
}
callback(MMS.MMS_PDU_STATUS_RETRIEVED, retrieved);
});
},
enumerable: true,
configurable: true,
writable: true
}
});
/**
* SendTransaction.
* Class for sending M-Send.req to MMSC, which inherits CancellableTransaction.
* @throws Error("Check max values parameters fail.")
*/
function SendTransaction(mmsConnection, cancellableId, msg, requestDeliveryReport) {
this.mmsConnection = mmsConnection;
// Call |CancellableTransaction| constructor.
CancellableTransaction.call(this, cancellableId, mmsConnection.serviceId);
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;
// Insert Phone number if available.
// Otherwise, Let MMS Proxy Relay insert from address automatically for us.
let phoneNumber = mmsConnection.getPhoneNumber();
let from = (phoneNumber) ? { address: phoneNumber, type: "PLMN" } : null;
msg.headers["from"] = from;
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-delivery-report"] = requestDeliveryReport;
if (!gMmsTransactionHelper.checkMaxValuesParameters(msg)) {
//We should notify end user that the header format is wrong.
if (DEBUG) debug("Check max values parameters fail.");
throw new Error("Check max values parameters fail.");
}
if (msg.parts) {
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;
}
if (DEBUG) debug("msg: " + JSON.stringify(msg));
this.msg = msg;
}
SendTransaction.prototype = Object.create(CancellableTransaction.prototype, {
istreamComposed: {
value: false,
enumerable: true,
configurable: true,
writable: true
},
/**
* @param parts
* 'parts' property of a parsed MMS message.
* @param callback [optional]
* A callback function that takes zero argument.
*/
loadBlobs: {
value: function(parts, callback) {
let callbackIfValid = () => {
if (DEBUG) debug("All parts loaded: " + JSON.stringify(parts));
if (callback) {
callback();
}
};
if (!parts || !parts.length) {
callbackIfValid();
return;
}
let numPartsToLoad = parts.length;
parts.forEach((aPart) => {
if (!(aPart.content instanceof Blob)) {
numPartsToLoad--;
if (!numPartsToLoad) {
callbackIfValid();
}
return;
}
let fileReader = new FileReader();
fileReader.addEventListener("loadend", (aEvent) => {
let arrayBuffer = aEvent.target.result;
aPart.content = new Uint8Array(arrayBuffer);
numPartsToLoad--;
if (!numPartsToLoad) {
callbackIfValid();
}
});
fileReader.readAsArrayBuffer(aPart.content);
});
},
enumerable: true,
configurable: true,
writable: true
},
/**
* @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: {
value: function(callback) {
this.registerRunCallback(callback);
if (!this.istreamComposed) {
this.loadBlobs(this.msg.parts, () => {
this.istream = MMS.PduHelper.compose(null, this.msg);
this.istreamSize = this.istream.available();
this.istreamComposed = true;
if (this.isCancelled) {
this.runCallbackIfValid(_MMS_ERROR_MESSAGE_DELETED, null);
} else {
this.run(callback);
}
});
return;
}
if (!this.istream) {
this.runCallbackIfValid(MMS.MMS_PDU_ERROR_PERMANENT_FAILURE, null);
return;
}
this.retryCount = 0;
let retryCallback = (mmsStatus, msg) => {
if ((MMS.MMS_PDU_ERROR_TRANSIENT_FAILURE == mmsStatus ||
MMS.MMS_PDU_ERROR_PERMANENT_FAILURE == mmsStatus) &&
this.retryCount < PREF_SEND_RETRY_COUNT) {
if (DEBUG) {
debug("Fail to send. Will retry after: " + PREF_SEND_RETRY_INTERVAL[this.retryCount]);
}
if (this.timer == null) {
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
}
// the input stream may be read in the previous failure request so
// we have to re-compose it.
if (this.istreamSize == null ||
this.istreamSize != this.istream.available()) {
this.istream = MMS.PduHelper.compose(null, this.msg);
}
this.timer.initWithCallback(() => this.send(retryCallback),
PREF_SEND_RETRY_INTERVAL[this.retryCount],
Ci.nsITimer.TYPE_ONE_SHOT);
this.retryCount++;
return;
}
this.runCallbackIfValid(mmsStatus, msg);
};
// This is the entry point to start sending.
this.send(retryCallback);
},
enumerable: true,
configurable: true,
writable: true
},
/**
* @param callback
* A callback function that takes two arguments: one for
* X-Mms-Response-Status, the other for the parsed M-Send.conf message.
*/
send: {
value: function(callback) {
this.timer = null;
this.cancellable =
gMmsTransactionHelper.sendRequest(this.mmsConnection,
"POST",
null,
this.istream,
(httpStatus, data) => {
let mmsStatus = gMmsTransactionHelper.
translateHttpStatusToMmsStatus(
httpStatus,
this.cancelledReason,
MMS.MMS_PDU_ERROR_TRANSIENT_FAILURE);
if (httpStatus != HTTP_STATUS_OK) {
callback(mmsStatus, null);
return;
}
if (!data) {
callback(MMS.MMS_PDU_ERROR_PERMANENT_FAILURE, null);
return;
}
let response = MMS.PduHelper.parse(data, null);
if (DEBUG) {
debug("Parsed M-Send.conf: " + JSON.stringify(response));
}
if (!response || (response.type != MMS.MMS_PDU_TYPE_SEND_CONF)) {
callback(MMS.MMS_PDU_RESPONSE_ERROR_UNSUPPORTED_MESSAGE, null);
return;
}
let responseStatus = response.headers["x-mms-response-status"];
callback(responseStatus, response);
});
},
enumerable: true,
configurable: true,
writable: true
}
});
/**
* Send M-acknowledge.ind back to MMSC.
*
* @param mmsConnection
* The MMS connection.
* @param transactionId
* X-Mms-Transaction-ID of the message.
* @param reportAllowed
* X-Mms-Report-Allowed of the response.
*
* @see OMA-TS-MMS_ENC-V1_3-20110913-A section 6.4
*/
function AcknowledgeTransaction(mmsConnection, transactionId, reportAllowed) {
this.mmsConnection = mmsConnection;
let headers = {};
// Mandatory fields
headers["x-mms-message-type"] = MMS.MMS_PDU_TYPE_ACKNOWLEDGE_IND;
headers["x-mms-transaction-id"] = transactionId;
headers["x-mms-mms-version"] = MMS.MMS_VERSION;
// Optional fields
headers["x-mms-report-allowed"] = reportAllowed;
this.istream = MMS.PduHelper.compose(null, {headers: headers});
}
AcknowledgeTransaction.prototype = {
/**
* @param callback [optional]
* A callback function that takes one argument -- the http status.
*/
run: function(callback) {
let requestCallback;
if (callback) {
requestCallback = (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.3 "Retrieving an MM".
callback(httpStatus);
};
}
gMmsTransactionHelper.sendRequest(this.mmsConnection,
"POST",
null,
this.istream,
requestCallback);
}
};
/**
* Return M-Read-Rec.ind back to MMSC
*
* @param messageID
* Message-ID of the message.
* @param toAddress
* The address of the recipient of the Read Report, i.e. the originator
* of the original multimedia message.
*
* @see OMA-TS-MMS_ENC-V1_3-20110913-A section 6.7.2
*/
function ReadRecTransaction(mmsConnection, messageID, toAddress) {
this.mmsConnection = mmsConnection;
let headers = {};
// Mandatory fields
headers["x-mms-message-type"] = MMS.MMS_PDU_TYPE_READ_REC_IND;
headers["x-mms-mms-version"] = MMS.MMS_VERSION;
headers["message-id"] = messageID;
let type = MMS.Address.resolveType(toAddress);
let to = {address: toAddress,
type: type}
headers["to"] = to;
// Insert Phone number if available.
// Otherwise, Let MMS Proxy Relay insert from address automatically for us.
let phoneNumber = mmsConnection.getPhoneNumber();
let from = (phoneNumber) ? { address: phoneNumber, type: "PLMN" } : null;
headers["from"] = from;
headers["x-mms-read-status"] = MMS.MMS_PDU_READ_STATUS_READ;
this.istream = MMS.PduHelper.compose(null, {headers: headers});
if (!this.istream) {
throw Cr.NS_ERROR_FAILURE;
}
}
ReadRecTransaction.prototype = {
run: function() {
gMmsTransactionHelper.sendRequest(this.mmsConnection,
"POST",
null,
this.istream,
null);
}
};
/**
* MmsService
*/
function MmsService() {
this._updateDebugFlag();
if (DEBUG) {
let macro = (MMS.MMS_VERSION >> 4) & 0x0f;
let minor = MMS.MMS_VERSION & 0x0f;
debug("Running protocol version: " + macro + "." + minor);
}
Services.prefs.addObserver(kPrefDefaultServiceId, this, false);
Services.prefs.addObserver(kPrefMmsDebuggingEnabled, this, false);
this.mmsDefaultServiceId = getDefaultServiceId();
// TODO: bug 810084 - support application identifier
}
MmsService.prototype = {
classID: GONK_MMSSERVICE_CID,
QueryInterface: XPCOMUtils.generateQI([Ci.nsIMmsService,
Ci.nsIWapPushApplication,
Ci.nsIObserver]),
/*
* 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,
_updateDebugFlag: function() {
try {
DEBUG = Services.prefs.getBoolPref(kPrefMmsDebuggingEnabled);
} catch (e) {}
},
/**
* Calculate Whether or not should we enable X-Mms-Report-Allowed.
*
* @param config
* Current configure value.
* @param wish
* Sender wish. Could be undefined, false, or true.
*/
getReportAllowed: function(config, wish) {
if ((config == CONFIG_SEND_REPORT_DEFAULT_NO)
|| (config == CONFIG_SEND_REPORT_DEFAULT_YES)) {
if (wish != null) {
config += (wish ? 1 : -1);
}
}
return config >= CONFIG_SEND_REPORT_DEFAULT_YES;
},
/**
* Convert intermediate message to indexedDB savable object.
*
* @param mmsConnection
* The MMS connection.
* @param retrievalMode
* Retrieval mode for MMS receiving setting.
* @param intermediate
* Intermediate MMS message parsed from PDU.
*/
convertIntermediateToSavable: function(mmsConnection, intermediate,
retrievalMode) {
intermediate.type = "mms";
intermediate.delivery = DELIVERY_NOT_DOWNLOADED;
let deliveryStatus;
switch (retrievalMode) {
case RETRIEVAL_MODE_MANUAL:
deliveryStatus = DELIVERY_STATUS_MANUAL;
break;
case RETRIEVAL_MODE_NEVER:
deliveryStatus = DELIVERY_STATUS_REJECTED;
break;
case RETRIEVAL_MODE_AUTOMATIC:
deliveryStatus = DELIVERY_STATUS_PENDING;
break;
case RETRIEVAL_MODE_AUTOMATIC_HOME:
if (mmsConnection.isVoiceRoaming()) {
deliveryStatus = DELIVERY_STATUS_MANUAL;
} else {
deliveryStatus = DELIVERY_STATUS_PENDING;
}
break;
default:
deliveryStatus = DELIVERY_STATUS_NOT_APPLICABLE;
break;
}
// |intermediate.deliveryStatus| will be deleted after being stored in db.
intermediate.deliveryStatus = deliveryStatus;
intermediate.timestamp = Date.now();
intermediate.receivers = [];
intermediate.phoneNumber = mmsConnection.getPhoneNumber();
intermediate.iccId = mmsConnection.getIccId();
return intermediate;
},
/**
* Merge the retrieval confirmation into the savable message.
*
* @param mmsConnection
* The MMS connection.
* @param intermediate
* Intermediate MMS message parsed from PDU, which carries
* the retrieval confirmation.
* @param savable
* The indexedDB savable MMS message, which is going to be
* merged with the extra retrieval confirmation.
*/
mergeRetrievalConfirmation: function(mmsConnection, intermediate, savable) {
// Prepare timestamp/sentTimestamp.
savable.timestamp = Date.now();
savable.sentTimestamp = intermediate.headers["date"].getTime();
savable.receivers = [];
// We don't have Bcc in recevied MMS message.
for (let type of ["cc", "to"]) {
if (intermediate.headers[type]) {
if (intermediate.headers[type] instanceof Array) {
for (let index in intermediate.headers[type]) {
savable.receivers.push(intermediate.headers[type][index].address);
}
} else {
savable.receivers.push(intermediate.headers[type].address);
}
}
}
savable.delivery = DELIVERY_RECEIVED;
// |savable.deliveryStatus| will be deleted after being stored in db.
savable.deliveryStatus = DELIVERY_STATUS_SUCCESS;
for (let field in intermediate.headers) {
savable.headers[field] = intermediate.headers[field];
}
if (intermediate.parts) {
savable.parts = intermediate.parts;
}
if (intermediate.content) {
savable.content = intermediate.content;
}
return savable;
},
/**
* @param aMmsConnection
* The MMS connection.
* @param aContentLocation
* X-Mms-Content-Location of the message.
* @param aCallback [optional]
* A callback function that takes two arguments: one for X-Mms-Status,
* the other parsed MMS message.
* @param aDomMessage
* The nsIMmsMessage object.
*/
retrieveMessage: function(aMmsConnection, aContentLocation, aCallback,
aDomMessage) {
// Notifying observers an MMS message is retrieving.
Services.obs.notifyObservers(aDomMessage, kSmsRetrievingObserverTopic, null);
let transaction = new RetrieveTransaction(aMmsConnection,
aDomMessage.id,
aContentLocation);
transaction.run(aCallback);
},
/**
* A helper to broadcast the system message to launch registered apps
* like Costcontrol, Notification and Message app... etc.
*
* @param aName
* The system message name.
* @param aDomMessage
* The nsIMmsMessage object.
*/
broadcastMmsSystemMessage: function(aName, aDomMessage) {
if (DEBUG) debug("Broadcasting the MMS system message: " + aName);
// Sadly we cannot directly broadcast the aDomMessage object
// because the system message mechamism will rewrap the object
// based on the content window, which needs to know the properties.
try {
gSystemMessenger.broadcastMessage(aName, {
iccId: aDomMessage.iccId,
type: aDomMessage.type,
id: aDomMessage.id,
threadId: aDomMessage.threadId,
delivery: aDomMessage.delivery,
deliveryInfo: aDomMessage.deliveryInfo,
sender: aDomMessage.sender,
receivers: aDomMessage.receivers,
timestamp: aDomMessage.timestamp,
sentTimestamp: aDomMessage.sentTimestamp,
read: aDomMessage.read,
subject: aDomMessage.subject,
smil: aDomMessage.smil,
attachments: aDomMessage.attachments,
expiryDate: aDomMessage.expiryDate,
readReportRequested: aDomMessage.readReportRequested
});
} catch (e) {
if (DEBUG) {
debug("Failed to _broadcastSmsSystemMessage: " + e);
}
}
},
/**
* A helper function to broadcast system message and notify observers that
* an MMS is sent.
*
* @params aDomMessage
* The nsIMmsMessage object.
*/
broadcastSentMessageEvent: function(aDomMessage) {
// Broadcasting a 'sms-sent' system message to open apps.
this.broadcastMmsSystemMessage(kSmsSentObserverTopic, aDomMessage);
// Notifying observers an MMS message is sent.
Services.obs.notifyObservers(aDomMessage, kSmsSentObserverTopic, null);
},
/**
* A helper function to broadcast system message and notify observers that
* an MMS is failed to send.
*
* @params aDomMessage
* The nsIMmsMessage object.
*/
broadcastSentFailureMessageEvent: function(aDomMessage) {
// Broadcasting a 'sms-sent' system message to open apps.
this.broadcastMmsSystemMessage(kSmsFailedObserverTopic, aDomMessage);
// Notifying observers an MMS message is sent.
Services.obs.notifyObservers(aDomMessage, kSmsFailedObserverTopic, null);
},
/**
* A helper function to broadcast system message and notify observers that
* an MMS is received.
*
* @params aDomMessage
* The nsIMmsMessage object.
*/
broadcastReceivedMessageEvent: function(aDomMessage) {
// Broadcasting a 'sms-received' system message to open apps.
this.broadcastMmsSystemMessage(kSmsReceivedObserverTopic, aDomMessage);
// Notifying observers an MMS message is received.
Services.obs.notifyObservers(aDomMessage, kSmsReceivedObserverTopic, null);
},
/**
* Callback for retrieveMessage.
*/
retrieveMessageCallback: function(mmsConnection, wish, savableMessage,
mmsStatus, retrievedMessage) {
if (DEBUG) debug("retrievedMessage = " + JSON.stringify(retrievedMessage));
let transactionId = savableMessage.headers["x-mms-transaction-id"];
// The absence of the field does not indicate any default
// value. So we go check the same field in the retrieved
// message instead.
if (wish == null && retrievedMessage) {
wish = retrievedMessage.headers["x-mms-delivery-report"];
}
let reportAllowed = this.getReportAllowed(this.confSendDeliveryReport,
wish);
// If the mmsStatus isn't MMS_PDU_STATUS_RETRIEVED after retrieving,
// something must be wrong with MMSC, so stop updating the DB record.
// We could send a message to content to notify the user the MMS
// retrieving failed. The end user has to retrieve the MMS again.
if (MMS.MMS_PDU_STATUS_RETRIEVED !== mmsStatus) {
if (mmsStatus != _MMS_ERROR_RADIO_DISABLED &&
mmsStatus != _MMS_ERROR_NO_SIM_CARD &&
mmsStatus != _MMS_ERROR_SIM_CARD_CHANGED) {
let transaction = new NotifyResponseTransaction(mmsConnection,
transactionId,
mmsStatus,
reportAllowed);
transaction.run();
}
// Retrieved fail after retry, so we update the delivery status in DB and
// notify this domMessage that error happen.
gMobileMessageDatabaseService
.setMessageDeliveryByMessageId(savableMessage.id,
null,
null,
DELIVERY_STATUS_ERROR,
null,
(aRv, aDomMessage) => {
let mmsMessage = null;
try {
mmsMessage = aDomMessage.QueryInterface(Ci.nsIMmsMessage);
} catch (e) {}
this.broadcastReceivedMessageEvent(mmsMessage);
});
return;
}
savableMessage = this.mergeRetrievalConfirmation(mmsConnection,
retrievedMessage,
savableMessage);
gMobileMessageDatabaseService.saveReceivedMessage(savableMessage,
(aRv, aDomMessage) => {
let mmsMessage = null;
try {
mmsMessage = aDomMessage.QueryInterface(Ci.nsIMmsMessage);
} catch (e) {}
let success = Components.isSuccessCode(aRv);
// Cite 6.2.1 "Transaction Flow" in OMA-TS-MMS_ENC-V1_3-20110913-A:
// The M-NotifyResp.ind response PDU SHALL provide a message retrieval
// status code. The status retrieved SHALL be used only if the MMS
// Client has successfully retrieved the MM prior to sending the
// NotifyResp.ind response PDU.
let transaction =
new NotifyResponseTransaction(mmsConnection,
transactionId,
success ? MMS.MMS_PDU_STATUS_RETRIEVED
: MMS.MMS_PDU_STATUS_DEFERRED,
reportAllowed);
transaction.run();
if (!success) {
// At this point we could send a message to content to notify the user
// that storing an incoming MMS failed, most likely due to a full disk.
// The end user has to retrieve the MMS again.
if (DEBUG) debug("Could not store MMS , error code " + aRv);
return;
}
this.broadcastReceivedMessageEvent(mmsMessage);
});
},
/**
* Callback for saveReceivedMessage.
*/
saveReceivedMessageCallback: function(mmsConnection, retrievalMode,
savableMessage, rv, domMessage) {
let success = Components.isSuccessCode(rv);
if (!success) {
// At this point we could send a message to content to notify the
// user that storing an incoming MMS notification indication failed,
// ost likely due to a full disk.
if (DEBUG) debug("Could not store MMS " + JSON.stringify(savableMessage) +
", error code " + rv);
// Because MMSC will resend the notification indication once we don't
// response the notification. Hope the end user will clean some space
// for the resent notification indication.
return;
}
// For X-Mms-Report-Allowed and X-Mms-Transaction-Id
let wish = savableMessage.headers["x-mms-delivery-report"];
let transactionId = savableMessage.headers["x-mms-transaction-id"];
this.broadcastReceivedMessageEvent(domMessage);
// To avoid costing money, we only send notify response when it's under
// the "automatic" retrieval mode or it's not in the roaming environment.
if (retrievalMode !== RETRIEVAL_MODE_AUTOMATIC &&
mmsConnection.isVoiceRoaming()) {
return;
}
if (RETRIEVAL_MODE_MANUAL === retrievalMode ||
RETRIEVAL_MODE_NEVER === retrievalMode) {
let mmsStatus = RETRIEVAL_MODE_NEVER === retrievalMode
? MMS.MMS_PDU_STATUS_REJECTED
: MMS.MMS_PDU_STATUS_DEFERRED;
// For X-Mms-Report-Allowed
let reportAllowed = this.getReportAllowed(this.confSendDeliveryReport,
wish);
let transaction = new NotifyResponseTransaction(mmsConnection,
transactionId,
mmsStatus,
reportAllowed);
transaction.run();
return;
}
let url = savableMessage.headers["x-mms-content-location"].uri;
// For RETRIEVAL_MODE_AUTOMATIC or RETRIEVAL_MODE_AUTOMATIC_HOME but not
// roaming, proceed to retrieve MMS.
this.retrieveMessage(mmsConnection,
url,
(aMmsStatus, aRetrievedMsg) =>
this.retrieveMessageCallback(mmsConnection,
wish,
savableMessage,
aMmsStatus,
aRetrievedMsg),
domMessage);
},
/**
* Handle incoming M-Notification.ind PDU.
*
* @param serviceId
* The ID of the service for receiving the PDU data.
* @param notification
* The parsed MMS message object.
*/
handleNotificationIndication: function(serviceId, notification) {
let transactionId = notification.headers["x-mms-transaction-id"];
gMobileMessageDatabaseService
.getMessageRecordByTransactionId(transactionId, (aRv, aMessageRecord) => {
if (Components.isSuccessCode(aRv) && aMessageRecord) {
if (DEBUG) debug("We already got the NotificationIndication with transactionId = "
+ transactionId + " before.");
return;
}
let retrievalMode = RETRIEVAL_MODE_MANUAL;
try {
retrievalMode = Services.prefs.getCharPref(kPrefRetrievalMode);
} catch (e) {}
// Under the "automatic"/"automatic-home" retrieval mode, we switch to
// the "manual" retrieval mode to download MMS for non-active SIM.
if ((retrievalMode == RETRIEVAL_MODE_AUTOMATIC ||
retrievalMode == RETRIEVAL_MODE_AUTOMATIC_HOME) &&
serviceId != this.mmsDefaultServiceId) {
if (DEBUG) {
debug("Switch to 'manual' mode to download MMS for non-active SIM: " +
"serviceId = " + serviceId + " doesn't equal to " +
"mmsDefaultServiceId = " + this.mmsDefaultServiceId);
}
retrievalMode = RETRIEVAL_MODE_MANUAL;
}
let mmsConnection = gMmsConnections.getConnByServiceId(serviceId);
let savableMessage = this.convertIntermediateToSavable(mmsConnection,
notification,
retrievalMode);
gMobileMessageDatabaseService
.saveReceivedMessage(savableMessage,
(aRv, aDomMessage) => {
let mmsMessage = null;
try {
mmsMessage = aDomMessage.QueryInterface(Ci.nsIMmsMessage);
} catch (e) {}
this.saveReceivedMessageCallback(mmsConnection,
retrievalMode,
savableMessage,
aRv,
mmsMessage);
});
});
},
/**
* Handle incoming M-Delivery.ind PDU.
*
* @param aMsg
* The MMS message object.
*/
handleDeliveryIndication: function(aMsg) {
let headers = aMsg.headers;
let envelopeId = headers["message-id"];
let address = headers.to.address;
let mmsStatus = headers["x-mms-status"];
if (DEBUG) {
debug("Start updating the delivery status for envelopeId: " + envelopeId +
" address: " + address + " mmsStatus: " + mmsStatus);
}
// From OMA-TS-MMS_ENC-V1_3-20110913-A subclause 9.3 "X-Mms-Status",
// in the M-Delivery.ind the X-Mms-Status could be MMS.MMS_PDU_STATUS_{
// EXPIRED, RETRIEVED, REJECTED, DEFERRED, UNRECOGNISED, INDETERMINATE,
// FORWARDED, UNREACHABLE }.
let deliveryStatus;
switch (mmsStatus) {
case MMS.MMS_PDU_STATUS_RETRIEVED:
deliveryStatus = DELIVERY_STATUS_SUCCESS;
break;
case MMS.MMS_PDU_STATUS_EXPIRED:
case MMS.MMS_PDU_STATUS_REJECTED:
case MMS.MMS_PDU_STATUS_UNRECOGNISED:
case MMS.MMS_PDU_STATUS_UNREACHABLE:
deliveryStatus = DELIVERY_STATUS_REJECTED;
break;
case MMS.MMS_PDU_STATUS_DEFERRED:
deliveryStatus = DELIVERY_STATUS_PENDING;
break;
case MMS.MMS_PDU_STATUS_INDETERMINATE:
deliveryStatus = DELIVERY_STATUS_NOT_APPLICABLE;
break;
default:
if (DEBUG) debug("Cannot handle this MMS status. Returning.");
return;
}
if (DEBUG) debug("Updating the delivery status to: " + deliveryStatus);
gMobileMessageDatabaseService
.setMessageDeliveryStatusByEnvelopeId(envelopeId, address, deliveryStatus,
(aRv, aDomMessage) => {
if (DEBUG) debug("Marking the delivery status is done.");
let mmsMessage = null;
try {
mmsMessage = aDomMessage.QueryInterface(Ci.nsIMmsMessage);
} catch (e) {}
// TODO bug 832140 handle !Components.isSuccessCode(aRv)
let topic;
if (mmsStatus === MMS.MMS_PDU_STATUS_RETRIEVED) {
topic = kSmsDeliverySuccessObserverTopic;
// Broadcasting a 'sms-delivery-success' system message to open apps.
this.broadcastMmsSystemMessage(topic, mmsMessage);
} else if (mmsStatus === MMS.MMS_PDU_STATUS_REJECTED) {
topic = kSmsDeliveryErrorObserverTopic;
// Broadcasting a 'sms-delivery-error' system message to open apps.
this.broadcastMmsSystemMessage(topic, mmsMessage);
} else {
if (DEBUG) debug("Needn't fire event for this MMS status. Returning.");
return;
}
// Notifying observers the delivery status is updated.
Services.obs.notifyObservers(mmsMessage, topic, null);
});
},
/**
* Handle incoming M-Read-Orig.ind PDU.
*
* @param aIndication
* The MMS message object.
*/
handleReadOriginateIndication: function(aIndication) {
let headers = aIndication.headers;
let envelopeId = headers["message-id"];
let address = headers.from.address;
let mmsReadStatus = headers["x-mms-read-status"];
if (DEBUG) {
debug("Start updating the read status for envelopeId: " + envelopeId +
", address: " + address + ", mmsReadStatus: " + mmsReadStatus);
}
// From OMA-TS-MMS_ENC-V1_3-20110913-A subclause 9.4 "X-Mms-Read-Status",
// in M-Read-Rec-Orig.ind the X-Mms-Read-Status could be
// MMS.MMS_READ_STATUS_{ READ, DELETED_WITHOUT_BEING_READ }.
let readStatus = mmsReadStatus == MMS.MMS_PDU_READ_STATUS_READ
? MMS.DOM_READ_STATUS_SUCCESS
: MMS.DOM_READ_STATUS_ERROR;
if (DEBUG) debug("Updating the read status to: " + readStatus);
gMobileMessageDatabaseService
.setMessageReadStatusByEnvelopeId(envelopeId, address, readStatus,
(aRv, aDomMessage) => {
let mmsMessage = null;
try {
mmsMessage = aDomMessage.QueryInterface(Ci.nsIMmsMessage);
} catch (e) {}
if (!Components.isSuccessCode(aRv)) {
if (DEBUG) debug("Failed to update read status: " + aRv);
return;
}
if (DEBUG) debug("Marking the read status is done.");
let topic;
if (mmsReadStatus == MMS.MMS_PDU_READ_STATUS_READ) {
topic = kSmsReadSuccessObserverTopic;
// Broadcasting a 'sms-read-success' system message to open apps.
this.broadcastMmsSystemMessage(topic, mmsMessage);
} else {
topic = kSmsReadErrorObserverTopic;
}
// Notifying observers the read status is updated.
Services.obs.notifyObservers(mmsMessage, topic, null);
});
},
/**
* A utility function to convert the MmsParameters dictionary object
* to a database-savable message.
*
* @param aMmsConnection
* The MMS connection.
* @param aParams
* The MmsParameters dictionay object.
* @param aMessage (output)
* The database-savable message.
* Return the error code by veryfying if the |aParams| is valid or not.
*
* Notes:
*
* OMA-TS-MMS-CONF-V1_3-20110913-A section 10.2.2 "Message Content Encoding":
*
* A name for multipart object SHALL be encoded using name-parameter for Content-Type
* header in WSP multipart headers. In decoding, name-parameter of Content-Type SHALL
* be used if available. If name-parameter of Content-Type is not available, filename
* parameter of Content-Disposition header SHALL be used if available. If neither
* name-parameter of Content-Type header nor filename parameter of Content-Disposition
* header is available, Content-Location header SHALL be used if available.
*/
createSavableFromParams: function(aMmsConnection, aParams, aMessage) {
if (DEBUG) debug("createSavableFromParams: aParams: " + JSON.stringify(aParams));
let isAddrValid = true;
let smil = aParams.smil;
// |aMessage.headers|
let headers = aMessage["headers"] = {};
let receivers = aParams.receivers;
let headersTo = headers["to"] = [];
if (receivers.length != 0) {
for (let i = 0; i < receivers.length; i++) {
let receiver = receivers[i];
let type = MMS.Address.resolveType(receiver);
let address;
if (type == "PLMN") {
address = PhoneNumberUtils.normalize(receiver, false);
if (!PhoneNumberUtils.isPlainPhoneNumber(address)) {
isAddrValid = false;
}
if (DEBUG) debug("createSavableFromParams: normalize phone number " +
"from " + receiver + " to " + address);
} else {
address = receiver;
if (type == "Others") {
isAddrValid = false;
if (DEBUG) debug("Error! Address is invalid to send MMS: " + address);
}
}
headersTo.push({"address": address, "type": type});
}
}
if (aParams.subject) {
headers["subject"] = aParams.subject;
}
// |aMessage.parts|
let attachments = aParams.attachments;
if (attachments.length != 0 || smil) {
let parts = aMessage["parts"] = [];
// Set the SMIL part if needed.
if (smil) {
let part = {
"headers": {
"content-type": {
"media": "application/smil",
"params": {
"name": "smil.xml",
"charset": {
"charset": "utf-8"
}
}
},
"content-location": "smil.xml",
"content-id": "<smil>"
},
"content": smil
};
parts.push(part);
}
// Set other parts for attachments if needed.
for (let i = 0; i < attachments.length; i++) {
let attachment = attachments[i];
let content = attachment.content;
let location = attachment.location;
let params = {
"name": location
};
if (content.type && content.type.indexOf("text/") == 0) {
params.charset = {
"charset": "utf-8"
};
}
let part = {
"headers": {
"content-type": {
"media": content.type,
"params": params
},
"content-location": location,
"content-id": attachment.id
},
"content": content
};
parts.push(part);
}
}
// The following attributes are needed for saving message into DB.
aMessage["type"] = "mms";
aMessage["timestamp"] = Date.now();
aMessage["receivers"] = receivers;
aMessage["sender"] = aMmsConnection.getPhoneNumber();
aMessage["iccId"] = aMmsConnection.getIccId();
try {
aMessage["deliveryStatusRequested"] =
Services.prefs.getBoolPref("dom.mms.requestStatusReport");
} catch (e) {
aMessage["deliveryStatusRequested"] = false;
}
try {
headers["x-mms-read-report"] =
Services.prefs.getBoolPref("dom.mms.requestReadReport");
} catch (e) {
headers["x-mms-read-report"] = false;
}
if (DEBUG) debug("createSavableFromParams: aMessage: " +
JSON.stringify(aMessage));
return isAddrValid ? Ci.nsIMobileMessageCallback.SUCCESS_NO_ERROR
: Ci.nsIMobileMessageCallback.INVALID_ADDRESS_ERROR;
},
// nsIMmsService
mmsDefaultServiceId: 0,
send: function(aServiceId, aParams, aRequest) {
if (DEBUG) debug("send: aParams: " + JSON.stringify(aParams));
// Note that the following sanity checks for |aParams| should be consistent
// with the checks in SmsIPCService.GetSendMmsMessageRequestFromParams.
// Check if |aParams| is valid.
if (aParams == null || typeof aParams != "object") {
if (DEBUG) debug("Error! 'aParams' should be a non-null object.");
throw Cr.NS_ERROR_INVALID_ARG;
return;
}
// Check if |receivers| is valid.
if (!Array.isArray(aParams.receivers)) {
if (DEBUG) debug("Error! 'receivers' should be an array.");
throw Cr.NS_ERROR_INVALID_ARG;
return;
}
// Check if |subject| is valid.
if (aParams.subject != null && typeof aParams.subject != "string") {
if (DEBUG) debug("Error! 'subject' should be a string if passed.");
throw Cr.NS_ERROR_INVALID_ARG;
return;
}
// Check if |smil| is valid.
if (aParams.smil != null && typeof aParams.smil != "string") {
if (DEBUG) debug("Error! 'smil' should be a string if passed.");
throw Cr.NS_ERROR_INVALID_ARG;
return;
}
// Check if |attachments| is valid.
if (!Array.isArray(aParams.attachments)) {
if (DEBUG) debug("Error! 'attachments' should be an array.");
throw Cr.NS_ERROR_INVALID_ARG;
return;
}
let sendTransactionCb = (aDomMessage, aErrorCode, aEnvelopeId) => {
if (DEBUG) {
debug("The returned status of sending transaction: " +
"aErrorCode: " + aErrorCode + " aEnvelopeId: " + aEnvelopeId);
}
// If the messsage has been deleted (because the sending process is
// cancelled), we don't need to reset the its delievery state/status.
if (aErrorCode == Ci.nsIMobileMessageCallback.NOT_FOUND_ERROR) {
aRequest.notifySendMessageFailed(aErrorCode, aDomMessage);
this.broadcastSentFailureMessageEvent(aDomMessage);
return;
}
let isSentSuccess = (aErrorCode == Ci.nsIMobileMessageCallback.SUCCESS_NO_ERROR);
gMobileMessageDatabaseService
.setMessageDeliveryByMessageId(aDomMessage.id,
null,
isSentSuccess ? DELIVERY_SENT : DELIVERY_ERROR,
isSentSuccess ? null : DELIVERY_STATUS_ERROR,
aEnvelopeId,
(aRv, aDomMessage) => {
if (DEBUG) debug("Marking the delivery state/staus is done. Notify sent or failed.");
let mmsMessage = null;
try {
mmsMessage = aDomMessage.QueryInterface(Ci.nsIMmsMessage);
} catch (e) {}
// TODO bug 832140 handle !Components.isSuccessCode(aRv)
if (!isSentSuccess) {
if (DEBUG) debug("Sending MMS failed.");
aRequest.notifySendMessageFailed(aErrorCode, mmsMessage);
this.broadcastSentFailureMessageEvent(mmsMessage);
return;
}
if (DEBUG) debug("Sending MMS succeeded.");
// Notifying observers the MMS message is sent.
this.broadcastSentMessageEvent(mmsMessage);
// Return the request after sending the MMS message successfully.
aRequest.notifyMessageSent(mmsMessage);
});
};
let mmsConnection = gMmsConnections.getConnByServiceId(aServiceId);
let savableMessage = {};
let errorCode = this.createSavableFromParams(mmsConnection, aParams,
savableMessage);
gMobileMessageDatabaseService
.saveSendingMessage(savableMessage,
(aRv, aDomMessage) => {
let mmsMessage = null;
try {
mmsMessage = aDomMessage.QueryInterface(Ci.nsIMmsMessage);
} catch (e) {}
if (!Components.isSuccessCode(aRv)) {
if (DEBUG) debug("Error! Fail to save sending message! rv = " + aRv);
aRequest.notifySendMessageFailed(
gMobileMessageDatabaseService.translateCrErrorToMessageCallbackError(aRv),
mmsMessage);
this.broadcastSentFailureMessageEvent(mmsMessage);
return;
}
if (DEBUG) debug("Saving sending message is done. Start to send.");
Services.obs.notifyObservers(mmsMessage, kSmsSendingObserverTopic, null);
if (errorCode !== Ci.nsIMobileMessageCallback.SUCCESS_NO_ERROR) {
if (DEBUG) debug("Error! The params for sending MMS are invalid.");
sendTransactionCb(mmsMessage, errorCode, null);
return;
}
// Check radio state in prior to default service Id.
if (isRadioOff(aServiceId)) {
if (DEBUG) debug("Error! Radio is disabled when sending MMS.");
sendTransactionCb(mmsMessage,
Ci.nsIMobileMessageCallback.RADIO_DISABLED_ERROR,
null);
return;
}
// To support DSDS, we have to stop users sending MMS when the selected
// SIM is not active, thus avoiding the data disconnection of the current
// SIM. Users have to manually swith the default SIM before sending.
if (mmsConnection.serviceId != this.mmsDefaultServiceId) {
if (DEBUG) debug("RIL service is not active to send MMS.");
sendTransactionCb(mmsMessage,
Ci.nsIMobileMessageCallback.NON_ACTIVE_SIM_CARD_ERROR,
null);
return;
}
// This is the entry point starting to send MMS.
let sendTransaction;
try {
sendTransaction =
new SendTransaction(mmsConnection, mmsMessage.id, savableMessage,
savableMessage["deliveryStatusRequested"]);
} catch (e) {
if (DEBUG) debug("Exception: fail to create a SendTransaction instance.");
sendTransactionCb(mmsMessage,
Ci.nsIMobileMessageCallback.INTERNAL_ERROR, null);
return;
}
sendTransaction.run((aMmsStatus, aMsg) => {
if (DEBUG) debug("The sending status of sendTransaction.run(): " + aMmsStatus);
let errorCode;
if (aMmsStatus == _MMS_ERROR_MESSAGE_DELETED) {
errorCode = Ci.nsIMobileMessageCallback.NOT_FOUND_ERROR;
} else if (aMmsStatus == _MMS_ERROR_RADIO_DISABLED) {
errorCode = Ci.nsIMobileMessageCallback.RADIO_DISABLED_ERROR;
} else if (aMmsStatus == _MMS_ERROR_NO_SIM_CARD) {
errorCode = Ci.nsIMobileMessageCallback.NO_SIM_CARD_ERROR;
} else if (aMmsStatus == _MMS_ERROR_SIM_CARD_CHANGED) {
errorCode = Ci.nsIMobileMessageCallback.NON_ACTIVE_SIM_CARD_ERROR;
} else if (aMmsStatus != MMS.MMS_PDU_ERROR_OK) {
errorCode = Ci.nsIMobileMessageCallback.INTERNAL_ERROR;
} else {
errorCode = Ci.nsIMobileMessageCallback.SUCCESS_NO_ERROR;
}
let envelopeId =
aMsg && aMsg.headers && aMsg.headers["message-id"] || null;
sendTransactionCb(mmsMessage, errorCode, envelopeId);
});
});
},
retrieve: function(aMessageId, aRequest) {
if (DEBUG) debug("Retrieving message with ID " + aMessageId);
gMobileMessageDatabaseService
.getMessageRecordById(aMessageId, (aRv, aMessageRecord, aDomMessage) => {
let mmsMessage = null;
try {
mmsMessage = aDomMessage.QueryInterface(Ci.nsIMmsMessage);
} catch (e) {}
if (!Components.isSuccessCode(aRv)) {
if (DEBUG) debug("Function getMessageRecordById() return error: " + aRv);
aRequest.notifyGetMessageFailed(
gMobileMessageDatabaseService.translateCrErrorToMessageCallbackError(aRv));
return;
}
if ("mms" != aMessageRecord.type) {
if (DEBUG) debug("Type of message record is not 'mms'.");
aRequest.notifyGetMessageFailed(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
return;
}
if (!aMessageRecord.headers) {
if (DEBUG) debug("Must need the MMS' headers to proceed the retrieve.");
aRequest.notifyGetMessageFailed(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
return;
}
if (!aMessageRecord.headers["x-mms-content-location"]) {
if (DEBUG) debug("Can't find mms content url in database.");
aRequest.notifyGetMessageFailed(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
return;
}
if (DELIVERY_NOT_DOWNLOADED != aMessageRecord.delivery) {
if (DEBUG) debug("Delivery of message record is not 'not-downloaded'.");
aRequest.notifyGetMessageFailed(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
return;
}
let deliveryStatus = aMessageRecord.deliveryInfo[0].deliveryStatus;
if (DELIVERY_STATUS_PENDING == deliveryStatus) {
if (DEBUG) debug("Delivery status of message record is 'pending'.");
aRequest.notifyGetMessageFailed(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
return;
}
// Cite 6.2 "Multimedia Message Notification" in OMA-TS-MMS_ENC-V1_3-20110913-A:
// The field has only one format, relative. The recipient client calculates
// this length of time relative to the time it receives the notification.
if (aMessageRecord.headers["x-mms-expiry"] != undefined) {
let expiryDate = aMessageRecord.timestamp +
aMessageRecord.headers["x-mms-expiry"] * 1000;
if (expiryDate < Date.now()) {
if (DEBUG) debug("The message to be retrieved is expired.");
aRequest.notifyGetMessageFailed(Ci.nsIMobileMessageCallback.NOT_FOUND_ERROR);
return;
}
}
// IccInfo in RadioInterface is not available when radio is off and
// NO_SIM_CARD_ERROR will be replied instead of RADIO_DISABLED_ERROR.
// Hence, for manual retrieving, instead of checking radio state later
// in MmsConnection.acquire(), We have to check radio state in prior to
// iccId to return the error correctly.
let numRadioInterfaces = gMobileConnectionService.numItems;
let isAllRadioOff = true;
for (let serviceId = 0; serviceId < numRadioInterfaces; serviceId++) {
isAllRadioOff &= isRadioOff(serviceId);
}
if (isAllRadioOff) {
if (DEBUG) debug("Error! Radio is disabled when retrieving MMS.");
aRequest.notifyGetMessageFailed(
Ci.nsIMobileMessageCallback.RADIO_DISABLED_ERROR);
return;
}
// Get MmsConnection based on the saved MMS message record's ICC ID,
// which could fail when the corresponding SIM card isn't installed.
let mmsConnection;
try {
mmsConnection = gMmsConnections.getConnByIccId(aMessageRecord.iccId);
} catch (e) {
if (DEBUG) debug("Failed to get connection by IccId. e= " + e);
let error = (e === _MMS_ERROR_SIM_NOT_MATCHED) ?
Ci.nsIMobileMessageCallback.SIM_NOT_MATCHED_ERROR :
Ci.nsIMobileMessageCallback.NO_SIM_CARD_ERROR;
aRequest.notifyGetMessageFailed(error);
return;
}
// To support DSDS, we have to stop users retrieving MMS when the needed
// SIM is not active, thus avoiding the data disconnection of the current
// SIM. Users have to manually swith the default SIM before retrieving.
if (mmsConnection.serviceId != this.mmsDefaultServiceId) {
if (DEBUG) debug("RIL service is not active to retrieve MMS.");
aRequest.notifyGetMessageFailed(Ci.nsIMobileMessageCallback.NON_ACTIVE_SIM_CARD_ERROR);
return;
}
let url = aMessageRecord.headers["x-mms-content-location"].uri;
// For X-Mms-Report-Allowed
let wish = aMessageRecord.headers["x-mms-delivery-report"];
let responseNotify = (mmsStatus, retrievedMsg) => {
// If the messsage has been deleted (because the retrieving process is
// cancelled), we don't need to reset the its delievery state/status.
if (mmsStatus == _MMS_ERROR_MESSAGE_DELETED) {
aRequest.notifyGetMessageFailed(Ci.nsIMobileMessageCallback.NOT_FOUND_ERROR);
return;
}
// If the mmsStatus is still MMS_PDU_STATUS_DEFERRED after retry,
// we should not store it into database and update its delivery
// status to 'error'.
if (MMS.MMS_PDU_STATUS_RETRIEVED !== mmsStatus) {
if (DEBUG) debug("RetrieveMessage fail after retry.");
let errorCode = Ci.nsIMobileMessageCallback.INTERNAL_ERROR;
if (mmsStatus == _MMS_ERROR_RADIO_DISABLED) {
errorCode = Ci.nsIMobileMessageCallback.RADIO_DISABLED_ERROR;
} else if (mmsStatus == _MMS_ERROR_NO_SIM_CARD) {
errorCode = Ci.nsIMobileMessageCallback.NO_SIM_CARD_ERROR;
} else if (mmsStatus == _MMS_ERROR_SIM_CARD_CHANGED) {
errorCode = Ci.nsIMobileMessageCallback.NON_ACTIVE_SIM_CARD_ERROR;
}
gMobileMessageDatabaseService
.setMessageDeliveryByMessageId(aMessageId,
null,
null,
DELIVERY_STATUS_ERROR,
null,
() => aRequest.notifyGetMessageFailed(errorCode));
return;
}
// In OMA-TS-MMS_ENC-V1_3, Table 5 in page 25. This header field
// (x-mms-transaction-id) SHALL be present when the MMS Proxy relay
// seeks an acknowledgement for the MM delivered though M-Retrieve.conf
// PDU during deferred retrieval. This transaction ID is used by the MMS
// Client and MMS Proxy-Relay to provide linkage between the originated
// M-Retrieve.conf and the response M-Acknowledge.ind PDUs.
let transactionId = retrievedMsg.headers["x-mms-transaction-id"];
// 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 && retrievedMsg) {
wish = retrievedMsg.headers["x-mms-delivery-report"];
}
let reportAllowed = this.getReportAllowed(this.confSendDeliveryReport,
wish);
if (DEBUG) debug("retrievedMsg = " + JSON.stringify(retrievedMsg));
aMessageRecord = this.mergeRetrievalConfirmation(mmsConnection,
retrievedMsg,
aMessageRecord);
gMobileMessageDatabaseService.saveReceivedMessage(aMessageRecord,
(aRv, aDomMessage) => {
let mmsMessage = null;
try {
mmsMessage = aDomMessage.QueryInterface(Ci.nsIMmsMessage);
} catch (e) {}
let success = Components.isSuccessCode(aRv);
if (!success) {
// At this point we could send a message to content to
// notify the user that storing an incoming MMS failed, most
// likely due to a full disk.
if (DEBUG) debug("Could not store MMS, error code " + aRv);
aRequest.notifyGetMessageFailed(
gMobileMessageDatabaseService.translateCrErrorToMessageCallbackError(aRv));
return;
}
// Notifying observers a new MMS message is retrieved.
this.broadcastReceivedMessageEvent(mmsMessage);
// Return the request after retrieving the MMS message successfully.
aRequest.notifyMessageGot(mmsMessage);
// Cite 6.3.1 "Transaction Flow" in OMA-TS-MMS_ENC-V1_3-20110913-A:
// If an acknowledgement is requested, the MMS Client SHALL respond
// with an M-Acknowledge.ind PDU to the MMS Proxy-Relay that supports
// the specific MMS Client. The M-Acknowledge.ind PDU confirms
// successful message retrieval to the MMS Proxy Relay.
let transaction = new AcknowledgeTransaction(mmsConnection,
transactionId,
reportAllowed);
transaction.run();
});
};
// Update the delivery status to pending in DB.
gMobileMessageDatabaseService
.setMessageDeliveryByMessageId(aMessageId,
null,
null,
DELIVERY_STATUS_PENDING,
null,
(rv) => {
let success = Components.isSuccessCode(rv);
if (!success) {
if (DEBUG) debug("Could not change the delivery status, error code " + rv);
aRequest.notifyGetMessageFailed(
gMobileMessageDatabaseService.translateCrErrorToMessageCallbackError(rv));
return;
}
this.retrieveMessage(mmsConnection,
url,
(aMmsStatus, aRetrievedMsg) =>
responseNotify(aMmsStatus, aRetrievedMsg),
mmsMessage);
});
});
},
sendReadReport: function(messageID, toAddress, iccId) {
if (DEBUG) {
debug("messageID: " + messageID + " toAddress: " +
JSON.stringify(toAddress));
}
// Get MmsConnection based on the saved MMS message record's ICC ID,
// which could fail when the corresponding SIM card isn't installed.
let mmsConnection;
try {
mmsConnection = gMmsConnections.getConnByIccId(iccId);
} catch (e) {
if (DEBUG) debug("Failed to get connection by IccId. e = " + e);
return;
}
try {
let transaction =
new ReadRecTransaction(mmsConnection, messageID, toAddress);
transaction.run();
} catch (e) {
if (DEBUG) debug("sendReadReport fail. e = " + e);
}
},
// nsIWapPushApplication
receiveWapPush: function(array, length, offset, options) {
let data = {array: array, offset: offset};
let msg = MMS.PduHelper.parse(data, null);
if (!msg) {
return false;
}
if (DEBUG) debug("receiveWapPush: msg = " + JSON.stringify(msg));
switch (msg.type) {
case MMS.MMS_PDU_TYPE_NOTIFICATION_IND:
this.handleNotificationIndication(options.serviceId, msg);
break;
case MMS.MMS_PDU_TYPE_DELIVERY_IND:
this.handleDeliveryIndication(msg);
break;
case MMS.MMS_PDU_TYPE_READ_ORIG_IND:
this.handleReadOriginateIndication(msg);
break;
default:
if (DEBUG) debug("Unsupported X-MMS-Message-Type: " + msg.type);
break;
}
},
// nsIObserver
observe: function(aSubject, aTopic, aData) {
switch (aTopic) {
case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID:
if (aData === kPrefDefaultServiceId) {
this.mmsDefaultServiceId = getDefaultServiceId();
} else if (aData === kPrefMmsDebuggingEnabled) {
this._updateDebugFlag();
}
break;
}
}
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MmsService]);