gecko-dev/dom/mobilemessage/gonk/MmsService.js
stone 15c96595d7 Bug 1235484 - Part 1: Refine radio state check in MmsService. r=bevistseng
--HG--
extra : transplant_source : %EEq%0D%DA%AF%97%83%F7%A0%0B%B3%0B%7C4%FF%B8%E3%D8%D8%F9
2016-01-20 11:39:06 +08:00

2737 lines
95 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- 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]);