diff options
author | Mike Dodd <mdodd@google.com> | 2015-08-11 11:16:59 -0700 |
---|---|---|
committer | Mike Dodd <mdodd@google.com> | 2015-08-12 12:47:26 -0700 |
commit | d3b009ae55651f1e60950342468e3c37fdeb0796 (patch) | |
tree | bc4b489af52d0e2521e21167d2ad76a47256f348 /src/android | |
parent | ef8c7abbcfc9c770385d6609a4b4bc70240ebdc4 (diff) | |
download | packages_apps_Messaging-d3b009ae55651f1e60950342468e3c37fdeb0796.tar.gz packages_apps_Messaging-d3b009ae55651f1e60950342468e3c37fdeb0796.tar.bz2 packages_apps_Messaging-d3b009ae55651f1e60950342468e3c37fdeb0796.zip |
Initial checkin of AOSP Messaging app.
b/23110861
Change-Id: I11db999bd10656801e618f78ab2b2ef74136fff1
Diffstat (limited to 'src/android')
44 files changed, 10359 insertions, 0 deletions
diff --git a/src/android/support/v7/mms/ApnException.java b/src/android/support/v7/mms/ApnException.java new file mode 100644 index 0000000..075d92d --- /dev/null +++ b/src/android/support/v7/mms/ApnException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +/** + * APN exception + */ +public class ApnException extends Exception { + + public ApnException() { + super(); + } + + public ApnException(String message) { + super(message); + } + + public ApnException(Throwable cause) { + super(cause); + } + + public ApnException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/android/support/v7/mms/ApnSettingsLoader.java b/src/android/support/v7/mms/ApnSettingsLoader.java new file mode 100644 index 0000000..ff8ed12 --- /dev/null +++ b/src/android/support/v7/mms/ApnSettingsLoader.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import java.util.List; + +/** + * Interface for loading APNs for default SMS SIM + */ +public interface ApnSettingsLoader { + /** + * Interface to represent the minimal information MMS lib needs from an APN + */ + interface Apn { + /** + * Get the MMSC URL string + * + * @return MMSC URL + */ + String getMmsc(); + + /** + * Get the MMS proxy host address + * + * @return MMS proxy + */ + String getMmsProxy(); + + /** + * Get the MMS proxy host port + * + * @return the port of MMS proxy + */ + int getMmsProxyPort(); + + /** + * Flag the APN as a successful APN to use + */ + void setSuccess(); + } + + /** + * Get a list possible APN matching the subId and APN name + * + * @param apnName the APN name + * @return a list of possible APNs + */ + List<Apn> get(String apnName); +} diff --git a/src/android/support/v7/mms/ApnsXmlParser.java b/src/android/support/v7/mms/ApnsXmlParser.java new file mode 100644 index 0000000..eeafcf6 --- /dev/null +++ b/src/android/support/v7/mms/ApnsXmlParser.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.content.ContentValues; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * Parser for built-in XML resource file for APN list + */ +class ApnsXmlParser extends MmsXmlResourceParser { + interface ApnProcessor { + void process(ContentValues apnValues); + } + + private static final String TAG_APNS = "apns"; + private static final String TAG_APN = "apn"; + + private final ApnProcessor mApnProcessor; + + private final ContentValues mValues = new ContentValues(); + + ApnsXmlParser(final XmlPullParser parser, final ApnProcessor apnProcessor) { + super(parser); + mApnProcessor = apnProcessor; + } + + // Parse one APN + @Override + protected void parseRecord() throws IOException, XmlPullParserException { + if (TAG_APN.equals(mInputParser.getName())) { + mValues.clear(); + // Collect all the attributes + for (int i = 0; i < mInputParser.getAttributeCount(); i++) { + final String key = mInputParser.getAttributeName(i); + if (key != null) { + mValues.put(key, mInputParser.getAttributeValue(i)); + } + } + // We are done parsing one APN, call the handler + if (mApnProcessor != null) { + mApnProcessor.process(mValues); + } + } + // We are at the end tag + if (mInputParser.next() != XmlPullParser.END_TAG) { + throw new XmlPullParserException("Expecting end tag @" + xmlParserDebugContext()); + } + } + + @Override + protected String getRootTag() { + return TAG_APNS; + } +} diff --git a/src/android/support/v7/mms/CarrierConfigValuesLoader.java b/src/android/support/v7/mms/CarrierConfigValuesLoader.java new file mode 100644 index 0000000..ad94fb2 --- /dev/null +++ b/src/android/support/v7/mms/CarrierConfigValuesLoader.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.os.Bundle; + +/** + * Loader for carrier dependent configuration values + */ +public interface CarrierConfigValuesLoader { + /** + * Get the carrier config values in a bundle + * + * @param subId the associated subscription ID for the carrier configuration + * @return a bundle of all the values + */ + Bundle get(int subId); + + // Configuration keys and default values + + /** Boolean value: if MMS is enabled */ + public static final String CONFIG_ENABLED_MMS = "enabledMMS"; + public static final boolean CONFIG_ENABLED_MMS_DEFAULT = true; + /** + * Boolean value: if transaction ID should be appended to + * the download URL of a single segment WAP push message + */ + public static final String CONFIG_ENABLED_TRANS_ID = "enabledTransID"; + public static final boolean CONFIG_ENABLED_TRANS_ID_DEFAULT = false; + /** + * Boolean value: if acknowledge or notify response to a download + * should be sent to the WAP push message's download URL + */ + public static final String CONFIG_ENABLED_NOTIFY_WAP_MMSC = "enabledNotifyWapMMSC"; + public static final boolean CONFIG_ENABLED_NOTIFY_WAP_MMSC_DEFAULT = false; + /** + * Boolean value: if phone number alias can be used + */ + public static final String CONFIG_ALIAS_ENABLED = "aliasEnabled"; + public static final boolean CONFIG_ALIAS_ENABLED_DEFAULT = false; + /** + * Boolean value: if audio is allowed in attachment + */ + public static final String CONFIG_ALLOW_ATTACH_AUDIO = "allowAttachAudio"; + public static final boolean CONFIG_ALLOW_ATTACH_AUDIO_DEFAULT = true; + /** + * Boolean value: if true, long sms messages are always sent as multi-part sms + * messages, with no checked limit on the number of segments. If false, then + * as soon as the user types a message longer than a single segment (i.e. 140 chars), + * the message will turn into and be sent as an mms message or separate, + * independent SMS messages (dependent on CONFIG_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES flag). + * This feature exists for carriers that don't support multi-part sms. + */ + public static final String CONFIG_ENABLE_MULTIPART_SMS = "enableMultipartSMS"; + public static final boolean CONFIG_ENABLE_MULTIPART_SMS_DEFAULT = true; + /** + * Boolean value: if SMS delivery report is supported + */ + public static final String CONFIG_ENABLE_SMS_DELIVERY_REPORTS = "enableSMSDeliveryReports"; + public static final boolean CONFIG_ENABLE_SMS_DELIVERY_REPORTS_DEFAULT = true; + /** + * Boolean value: if group MMS is supported + */ + public static final String CONFIG_ENABLE_GROUP_MMS = "enableGroupMms"; + public static final boolean CONFIG_ENABLE_GROUP_MMS_DEFAULT = true; + /** + * Boolean value: if the content_disposition field of an MMS part should be parsed + * Check wap-230-wsp-20010705-a.pdf, chapter 8.4.2.21. Most carriers support it except some. + */ + public static final String CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION = + "supportMmsContentDisposition"; + public static final boolean CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION_DEFAULT = true; + /** + * Boolean value: if the sms app should support a link to the system settings + * where amber alerts are configured. + */ + public static final String CONFIG_CELL_BROADCAST_APP_LINKS = "config_cellBroadcastAppLinks"; + public static final boolean CONFIG_CELL_BROADCAST_APP_LINKS_DEFAULT = true; + /** + * Boolean value: if multipart SMS should be sent as separate SMS messages + */ + public static final String CONFIG_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES = + "sendMultipartSmsAsSeparateMessages"; + public static final boolean CONFIG_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES_DEFAULT = false; + /** + * Boolean value: if MMS read report is supported + */ + public static final String CONFIG_ENABLE_MMS_READ_REPORTS = "enableMMSReadReports"; + public static final boolean CONFIG_ENABLE_MMS_READ_REPORTS_DEFAULT = false; + /** + * Boolean value: if MMS delivery report is supported + */ + public static final String CONFIG_ENABLE_MMS_DELIVERY_REPORTS = "enableMMSDeliveryReports"; + public static final boolean CONFIG_ENABLE_MMS_DELIVERY_REPORTS_DEFAULT = false; + /** + * Boolean value: if "charset" value is supported in the "Content-Type" HTTP header + */ + public static final String CONFIG_SUPPORT_HTTP_CHARSET_HEADER = "supportHttpCharsetHeader"; + public static final boolean CONFIG_SUPPORT_HTTP_CHARSET_HEADER_DEFAULT = false; + /** + * Integer value: maximal MMS message size in bytes + */ + public static final String CONFIG_MAX_MESSAGE_SIZE = "maxMessageSize"; + public static final int CONFIG_MAX_MESSAGE_SIZE_DEFAULT = 300 * 1024; + /** + * Integer value: maximal MMS image height in pixels + */ + public static final String CONFIG_MAX_IMAGE_HEIGHT = "maxImageHeight"; + public static final int CONFIG_MAX_IMAGE_HEIGHT_DEFAULT = 480; + /** + * Integer value: maximal MMS image width in pixels + */ + public static final String CONFIG_MAX_IMAGE_WIDTH = "maxImageWidth"; + public static final int CONFIG_MAX_IMAGE_WIDTH_DEFAULT = 640; + /** + * Integer value: limit on recipient list of an MMS message + */ + public static final String CONFIG_RECIPIENT_LIMIT = "recipientLimit"; + public static final int CONFIG_RECIPIENT_LIMIT_DEFAULT = Integer.MAX_VALUE; + /** + * Integer value: HTTP socket timeout in milliseconds for MMS + */ + public static final String CONFIG_HTTP_SOCKET_TIMEOUT = "httpSocketTimeout"; + public static final int CONFIG_HTTP_SOCKET_TIMEOUT_DEFAULT = 60 * 1000; + /** + * Integer value: minimal number of characters of an alias + */ + public static final String CONFIG_ALIAS_MIN_CHARS = "aliasMinChars"; + public static final int CONFIG_ALIAS_MIN_CHARS_DEFAULT = 2; + /** + * Integer value: maximal number of characters of an alias + */ + public static final String CONFIG_ALIAS_MAX_CHARS = "aliasMaxChars"; + public static final int CONFIG_ALIAS_MAX_CHARS_DEFAULT = 48; + /** + * Integer value: the threshold of number of SMS parts when an multipart SMS will be + * converted into an MMS, e.g. if this is "4", when an multipart SMS message has 5 + * parts, then it will be sent as MMS message instead. "-1" indicates no such conversion + * can happen. + */ + public static final String CONFIG_SMS_TO_MMS_TEXT_THRESHOLD = "smsToMmsTextThreshold"; + public static final int CONFIG_SMS_TO_MMS_TEXT_THRESHOLD_DEFAULT = -1; + /** + * Integer value: the threshold of SMS length when it will be converted into an MMS. + * "-1" indicates no such conversion can happen. + */ + public static final String CONFIG_SMS_TO_MMS_TEXT_LENGTH_THRESHOLD = + "smsToMmsTextLengthThreshold"; + public static final int CONFIG_SMS_TO_MMS_TEXT_LENGTH_THRESHOLD_DEFAULT = -1; + /** + * Integer value: maximal length in bytes of SMS message + */ + public static final String CONFIG_MAX_MESSAGE_TEXT_SIZE = "maxMessageTextSize"; + public static final int CONFIG_MAX_MESSAGE_TEXT_SIZE_DEFAULT = -1; + /** + * Integer value: maximum number of characters allowed for mms subject + */ + public static final String CONFIG_MAX_SUBJECT_LENGTH = "maxSubjectLength"; + public static final int CONFIG_MAX_SUBJECT_LENGTH_DEFAULT = 40; + /** + * String value: name for the user agent profile HTTP header + */ + public static final String CONFIG_UA_PROF_TAG_NAME = "mUaProfTagName"; + public static final String CONFIG_UA_PROF_TAG_NAME_DEFAULT = "x-wap-profile"; + /** + * String value: additional HTTP headers for MMS HTTP requests. + * The format is + * header_1:header_value_1|header_2:header_value_2|... + * Each value can contain macros. + */ + public static final String CONFIG_HTTP_PARAMS = "httpParams"; + public static final String CONFIG_HTTP_PARAMS_DEFAULT = null; + /** + * String value: number of email gateway + */ + public static final String CONFIG_EMAIL_GATEWAY_NUMBER = "emailGatewayNumber"; + public static final String CONFIG_EMAIL_GATEWAY_NUMBER_DEFAULT = null; + /** + * String value: suffix for the NAI HTTP header value, e.g. ":pcs" + * (NAI is used as authentication in HTTP headers for some carriers) + */ + public static final String CONFIG_NAI_SUFFIX = "naiSuffix"; + public static final String CONFIG_NAI_SUFFIX_DEFAULT = null; +} diff --git a/src/android/support/v7/mms/CarrierConfigXmlParser.java b/src/android/support/v7/mms/CarrierConfigXmlParser.java new file mode 100644 index 0000000..42e7c21 --- /dev/null +++ b/src/android/support/v7/mms/CarrierConfigXmlParser.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * XML parser for carrier config (i.e. mms_config) + */ +class CarrierConfigXmlParser extends MmsXmlResourceParser { + interface KeyValueProcessor { + void process(String type, String key, String value); + } + + private static final String TAG_MMS_CONFIG = "mms_config"; + + private final KeyValueProcessor mKeyValueProcessor; + + CarrierConfigXmlParser(final XmlPullParser parser, final KeyValueProcessor keyValueProcessor) { + super(parser); + mKeyValueProcessor = keyValueProcessor; + } + + // Parse one key/value + @Override + protected void parseRecord() throws IOException, XmlPullParserException { + final String key = mInputParser.getAttributeValue(null, "name"); + // We are at the start tag, the name of the tag is the type + // e.g. <int name="key">value</int> + final String type = mInputParser.getName(); + int nextEvent = mInputParser.next(); + String value = null; + if (nextEvent == XmlPullParser.TEXT) { + value = mInputParser.getText(); + nextEvent = mInputParser.next(); + } + if (nextEvent != XmlPullParser.END_TAG) { + throw new XmlPullParserException("Expecting end tag @" + xmlParserDebugContext()); + } + // We are done parsing one mms_config key/value, call the handler + if (mKeyValueProcessor != null) { + mKeyValueProcessor.process(type, key, value); + } + } + + @Override + protected String getRootTag() { + return TAG_MMS_CONFIG; + } + +} diff --git a/src/android/support/v7/mms/DefaultApnSettingsLoader.java b/src/android/support/v7/mms/DefaultApnSettingsLoader.java new file mode 100644 index 0000000..83f2fd3 --- /dev/null +++ b/src/android/support/v7/mms/DefaultApnSettingsLoader.java @@ -0,0 +1,497 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.content.ContentValues; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; +import android.net.Uri; +import android.provider.Telephony; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; + +import com.android.messaging.R; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +/** + * Default implementation of APN settings loader + */ +class DefaultApnSettingsLoader implements ApnSettingsLoader { + /** + * The base implementation of an APN + */ + private static class BaseApn implements Apn { + /** + * Create a base APN from parameters + * + * @param typesIn the APN type field + * @param mmscIn the APN mmsc field + * @param proxyIn the APN mmsproxy field + * @param portIn the APN mmsport field + * @return an instance of base APN, or null if any of the parameter is invalid + */ + public static BaseApn from(final String typesIn, final String mmscIn, final String proxyIn, + final String portIn) { + if (!isValidApnType(trimWithNullCheck(typesIn), APN_TYPE_MMS)) { + return null; + } + String mmsc = trimWithNullCheck(mmscIn); + if (TextUtils.isEmpty(mmsc)) { + return null; + } + mmsc = trimV4AddrZeros(mmsc); + try { + new URI(mmsc); + } catch (final URISyntaxException e) { + return null; + } + String mmsProxy = trimWithNullCheck(proxyIn); + int mmsProxyPort = 80; + if (!TextUtils.isEmpty(mmsProxy)) { + mmsProxy = trimV4AddrZeros(mmsProxy); + final String portString = trimWithNullCheck(portIn); + if (portString != null) { + try { + mmsProxyPort = Integer.parseInt(portString); + } catch (final NumberFormatException e) { + // Ignore, just use 80 to try + } + } + } + return new BaseApn(mmsc, mmsProxy, mmsProxyPort); + } + + private final String mMmsc; + private final String mMmsProxy; + private final int mMmsProxyPort; + + public BaseApn(final String mmsc, final String proxy, final int port) { + mMmsc = mmsc; + mMmsProxy = proxy; + mMmsProxyPort = port; + } + + @Override + public String getMmsc() { + return mMmsc; + } + + @Override + public String getMmsProxy() { + return mMmsProxy; + } + + @Override + public int getMmsProxyPort() { + return mMmsProxyPort; + } + + @Override + public void setSuccess() { + // Do nothing + } + + public boolean equals(final BaseApn other) { + return TextUtils.equals(mMmsc, other.getMmsc()) && + TextUtils.equals(mMmsProxy, other.getMmsProxy()) && + mMmsProxyPort == other.getMmsProxyPort(); + } + } + + /** + * An in-memory implementation of an APN. These APNs are organized into an in-memory list. + * The order of the list can be changed by the setSuccess method. + */ + private static class MemoryApn implements Apn { + /** + * Create an in-memory APN loaded from resources + * + * @param apns the in-memory APN list + * @param typesIn the APN type field + * @param mmscIn the APN mmsc field + * @param proxyIn the APN mmsproxy field + * @param portIn the APN mmsport field + * @return an in-memory APN instance, null if there is invalid parameter + */ + public static MemoryApn from(final List<Apn> apns, final String typesIn, + final String mmscIn, final String proxyIn, final String portIn) { + if (apns == null) { + return null; + } + final BaseApn base = BaseApn.from(typesIn, mmscIn, proxyIn, portIn); + if (base == null) { + return null; + } + for (final Apn apn : apns) { + if (apn instanceof MemoryApn && ((MemoryApn) apn).equals(base)) { + return null; + } + } + return new MemoryApn(apns, base); + } + + private final List<Apn> mApns; + private final BaseApn mBase; + + public MemoryApn(final List<Apn> apns, final BaseApn base) { + mApns = apns; + mBase = base; + } + + @Override + public String getMmsc() { + return mBase.getMmsc(); + } + + @Override + public String getMmsProxy() { + return mBase.getMmsProxy(); + } + + @Override + public int getMmsProxyPort() { + return mBase.getMmsProxyPort(); + } + + @Override + public void setSuccess() { + // If this is being marked as a successful APN, move it to the top of the list so + // next time it will be tried first + boolean moved = false; + synchronized (mApns) { + if (mApns.get(0) != this) { + mApns.remove(this); + mApns.add(0, this); + moved = true; + } + } + if (moved) { + Log.d(MmsService.TAG, "Set APN [" + + "MMSC=" + getMmsc() + ", " + + "PROXY=" + getMmsProxy() + ", " + + "PORT=" + getMmsProxyPort() + "] to be first"); + } + } + + public boolean equals(final BaseApn other) { + if (other == null) { + return false; + } + return mBase.equals(other); + } + } + + /** + * APN_TYPE_ALL is a special type to indicate that this APN entry can + * service all data connections. + */ + public static final String APN_TYPE_ALL = "*"; + /** APN type for MMS traffic */ + public static final String APN_TYPE_MMS = "mms"; + + private static final String[] APN_PROJECTION = { + Telephony.Carriers.TYPE, + Telephony.Carriers.MMSC, + Telephony.Carriers.MMSPROXY, + Telephony.Carriers.MMSPORT, + }; + private static final int COLUMN_TYPE = 0; + private static final int COLUMN_MMSC = 1; + private static final int COLUMN_MMSPROXY = 2; + private static final int COLUMN_MMSPORT = 3; + + private static final String APN_MCC = "mcc"; + private static final String APN_MNC = "mnc"; + private static final String APN_APN = "apn"; + private static final String APN_TYPE = "type"; + private static final String APN_MMSC = "mmsc"; + private static final String APN_MMSPROXY = "mmsproxy"; + private static final String APN_MMSPORT = "mmsport"; + + private final Context mContext; + + // Cached APNs for subIds + private final SparseArray<List<Apn>> mApnsCache; + + DefaultApnSettingsLoader(final Context context) { + mContext = context; + mApnsCache = new SparseArray<>(); + } + + @Override + public List<Apn> get(final String apnName) { + final int subId = Utils.getEffectiveSubscriptionId(MmsManager.DEFAULT_SUB_ID); + List<Apn> apns; + boolean didLoad = false; + synchronized (this) { + apns = mApnsCache.get(subId); + if (apns == null) { + apns = new ArrayList<>(); + mApnsCache.put(subId, apns); + loadLocked(subId, apnName, apns); + didLoad = true; + } + } + if (didLoad) { + Log.i(MmsService.TAG, "Loaded " + apns.size() + " APNs"); + } + return apns; + } + + private void loadLocked(final int subId, final String apnName, final List<Apn> apns) { + // Try system APN table first + loadFromSystem(subId, apnName, apns); + if (apns.size() > 0) { + return; + } + // Try loading from apns.xml in resources + loadFromResources(subId, apnName, apns); + if (apns.size() > 0) { + return; + } + // Try resources but without APN name + loadFromResources(subId, null/*apnName*/, apns); + } + + /** + * Load matching APNs from telephony provider. + * We try different combinations of the query to work around some platform quirks. + * + * @param subId the SIM subId + * @param apnName the APN name to match + * @param apns the list used to return results + */ + private void loadFromSystem(final int subId, final String apnName, final List<Apn> apns) { + Uri uri; + if (Utils.supportMSim() && subId != MmsManager.DEFAULT_SUB_ID) { + uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "/subId/" + subId); + } else { + uri = Telephony.Carriers.CONTENT_URI; + } + Cursor cursor = null; + try { + for (; ; ) { + // Try different combinations of queries. Some would work on some platforms. + // So we query each combination until we find one returns non-empty result. + cursor = querySystem(uri, true/*checkCurrent*/, apnName); + if (cursor != null) { + break; + } + cursor = querySystem(uri, false/*checkCurrent*/, apnName); + if (cursor != null) { + break; + } + cursor = querySystem(uri, true/*checkCurrent*/, null/*apnName*/); + if (cursor != null) { + break; + } + cursor = querySystem(uri, false/*checkCurrent*/, null/*apnName*/); + break; + } + } catch (final SecurityException e) { + // Can't access platform APN table, return directly + return; + } + if (cursor == null) { + return; + } + try { + if (cursor.moveToFirst()) { + final Apn apn = BaseApn.from( + cursor.getString(COLUMN_TYPE), + cursor.getString(COLUMN_MMSC), + cursor.getString(COLUMN_MMSPROXY), + cursor.getString(COLUMN_MMSPORT)); + if (apn != null) { + apns.add(apn); + } + } + } finally { + cursor.close(); + } + } + + /** + * Query system APN table + * + * @param uri The APN query URL to use + * @param checkCurrent If add "CURRENT IS NOT NULL" condition + * @param apnName The optional APN name for query condition + * @return A cursor of the query result. If a cursor is returned as not null, it is + * guaranteed to contain at least one row. + */ + private Cursor querySystem(final Uri uri, final boolean checkCurrent, String apnName) { + Log.i(MmsService.TAG, "Loading APNs from system, " + + "checkCurrent=" + checkCurrent + " apnName=" + apnName); + final StringBuilder selectionBuilder = new StringBuilder(); + String[] selectionArgs = null; + if (checkCurrent) { + selectionBuilder.append(Telephony.Carriers.CURRENT).append(" IS NOT NULL"); + } + apnName = trimWithNullCheck(apnName); + if (!TextUtils.isEmpty(apnName)) { + if (selectionBuilder.length() > 0) { + selectionBuilder.append(" AND "); + } + selectionBuilder.append(Telephony.Carriers.APN).append("=?"); + selectionArgs = new String[] { apnName }; + } + try { + final Cursor cursor = mContext.getContentResolver().query( + uri, + APN_PROJECTION, + selectionBuilder.toString(), + selectionArgs, + null/*sortOrder*/); + if (cursor == null || cursor.getCount() < 1) { + if (cursor != null) { + cursor.close(); + } + Log.w(MmsService.TAG, "Query " + uri + " with apn " + apnName + " and " + + (checkCurrent ? "checking CURRENT" : "not checking CURRENT") + + " returned empty"); + return null; + } + return cursor; + } catch (final SQLiteException e) { + Log.w(MmsService.TAG, "APN table query exception: " + e); + } catch (final SecurityException e) { + Log.w(MmsService.TAG, "Platform restricts APN table access: " + e); + throw e; + } + return null; + } + + /** + * Find matching APNs using builtin APN list resource + * + * @param subId the SIM subId + * @param apnName the APN name to match + * @param apns the list for returning results + */ + private void loadFromResources(final int subId, final String apnName, final List<Apn> apns) { + Log.i(MmsService.TAG, "Loading APNs from resources, apnName=" + apnName); + final int[] mccMnc = Utils.getMccMnc(mContext, subId); + if (mccMnc[0] == 0 && mccMnc[0] == 0) { + Log.w(MmsService.TAG, "Can not get valid mcc/mnc from system"); + return; + } + // MCC/MNC is good, loading/querying APNs from XML + XmlResourceParser xml = null; + try { + xml = mContext.getResources().getXml(R.xml.apns); + new ApnsXmlParser(xml, new ApnsXmlParser.ApnProcessor() { + @Override + public void process(ContentValues apnValues) { + final String mcc = trimWithNullCheck(apnValues.getAsString(APN_MCC)); + final String mnc = trimWithNullCheck(apnValues.getAsString(APN_MNC)); + final String apn = trimWithNullCheck(apnValues.getAsString(APN_APN)); + try { + if (mccMnc[0] == Integer.parseInt(mcc) && + mccMnc[1] == Integer.parseInt(mnc) && + (TextUtils.isEmpty(apnName) || apnName.equalsIgnoreCase(apn))) { + final String type = apnValues.getAsString(APN_TYPE); + final String mmsc = apnValues.getAsString(APN_MMSC); + final String mmsproxy = apnValues.getAsString(APN_MMSPROXY); + final String mmsport = apnValues.getAsString(APN_MMSPORT); + final Apn newApn = MemoryApn.from(apns, type, mmsc, mmsproxy, mmsport); + if (newApn != null) { + apns.add(newApn); + } + } + } catch (final NumberFormatException e) { + // Ignore + } + } + }).parse(); + } catch (final Resources.NotFoundException e) { + Log.w(MmsService.TAG, "Can not get apns.xml " + e); + } finally { + if (xml != null) { + xml.close(); + } + } + } + + private static String trimWithNullCheck(final String value) { + return value != null ? value.trim() : null; + } + + /** + * Trim leading zeros from IPv4 address strings + * Our base libraries will interpret that as octel.. + * Must leave non v4 addresses and host names alone. + * For example, 192.168.000.010 -> 192.168.0.10 + * + * @param addr a string representing an ip addr + * @return a string propertly trimmed + */ + private static String trimV4AddrZeros(final String addr) { + if (addr == null) { + return null; + } + final String[] octets = addr.split("\\."); + if (octets.length != 4) { + return addr; + } + final StringBuilder builder = new StringBuilder(16); + String result = null; + for (int i = 0; i < 4; i++) { + try { + if (octets[i].length() > 3) { + return addr; + } + builder.append(Integer.parseInt(octets[i])); + } catch (final NumberFormatException e) { + return addr; + } + if (i < 3) { + builder.append('.'); + } + } + result = builder.toString(); + return result; + } + + /** + * Check if the APN contains the APN type we want + * + * @param types The string encodes a list of supported types + * @param requestType The type we want + * @return true if the input types string contains the requestType + */ + public static boolean isValidApnType(final String types, final String requestType) { + // If APN type is unspecified, assume APN_TYPE_ALL. + if (TextUtils.isEmpty(types)) { + return true; + } + for (final String t : types.split(",")) { + if (t.equals(requestType) || t.equals(APN_TYPE_ALL)) { + return true; + } + } + return false; + } +} diff --git a/src/android/support/v7/mms/DefaultCarrierConfigValuesLoader.java b/src/android/support/v7/mms/DefaultCarrierConfigValuesLoader.java new file mode 100644 index 0000000..1d45f8f --- /dev/null +++ b/src/android/support/v7/mms/DefaultCarrierConfigValuesLoader.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.os.Bundle; +import android.util.Log; +import android.util.SparseArray; + +import com.android.messaging.R; + +/** + * The default implementation of loader for carrier config values + */ +class DefaultCarrierConfigValuesLoader implements CarrierConfigValuesLoader { + /* + * Key types + */ + public static final String KEY_TYPE_INT = "int"; + public static final String KEY_TYPE_BOOL = "bool"; + public static final String KEY_TYPE_STRING = "string"; + + private final Context mContext; + + // Cached values for subIds + private final SparseArray<Bundle> mValuesCache; + + DefaultCarrierConfigValuesLoader(final Context context) { + mContext = context; + mValuesCache = new SparseArray<>(); + } + + @Override + public Bundle get(int subId) { + subId = Utils.getEffectiveSubscriptionId(subId); + Bundle values; + boolean didLoad = false; + synchronized (this) { + values = mValuesCache.get(subId); + if (values == null) { + values = new Bundle(); + mValuesCache.put(subId, values); + loadLocked(subId, values); + didLoad = true; + } + } + if (didLoad) { + Log.i(MmsService.TAG, "Carrier configs loaded: " + values); + } + return values; + } + + private void loadLocked(final int subId, final Bundle values) { + // For K and earlier, load from resources + loadFromResources(subId, values); + if (Utils.hasMmsApi()) { + // For L and later, also load from system MMS service + loadFromSystem(subId, values); + } + } + + /** + * Load from system, using MMS API + * + * @param subId which SIM to load for + * @param values the result to add to + */ + private static void loadFromSystem(final int subId, final Bundle values) { + try { + final Bundle systemValues = Utils.getSmsManager(subId).getCarrierConfigValues(); + if (systemValues != null) { + values.putAll(systemValues); + } + } catch (final Exception e) { + Log.w(MmsService.TAG, "Calling system getCarrierConfigValues exception", e); + } + } + + private void loadFromResources(final int subId, final Bundle values) { + // Get a subscription-dependent context for loading the mms_config.xml + final Context subContext = Utils.getSubDepContext(mContext, subId); + XmlResourceParser xml = null; + try { + xml = subContext.getResources().getXml(R.xml.mms_config); + new CarrierConfigXmlParser(xml, new CarrierConfigXmlParser.KeyValueProcessor() { + @Override + public void process(String type, String key, String value) { + try { + if (KEY_TYPE_INT.equals(type)) { + values.putInt(key, Integer.parseInt(value)); + } else if (KEY_TYPE_BOOL.equals(type)) { + values.putBoolean(key, Boolean.parseBoolean(value)); + } else if (KEY_TYPE_STRING.equals(type)) { + values.putString(key, value); + } + } catch (final NumberFormatException e) { + Log.w(MmsService.TAG, "Load carrier value from resources: " + + "invalid " + key + "," + value + "," + type); + } + } + }).parse(); + } catch (final Resources.NotFoundException e) { + Log.w(MmsService.TAG, "Can not get mms_config.xml"); + } finally { + if (xml != null) { + xml.close(); + } + } + } +} diff --git a/src/android/support/v7/mms/DefaultUserAgentInfoLoader.java b/src/android/support/v7/mms/DefaultUserAgentInfoLoader.java new file mode 100644 index 0000000..e59a234 --- /dev/null +++ b/src/android/support/v7/mms/DefaultUserAgentInfoLoader.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.content.Context; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +/** + * The default implementation of loader of UA and UAProfUrl + */ +class DefaultUserAgentInfoLoader implements UserAgentInfoLoader { + // Default values to be used as user agent info + private static final String DEFAULT_USER_AGENT = "Android MmsLib/1.0"; + private static final String DEFAULT_UA_PROF_URL = + "http://www.gstatic.com/android/sms/mms_ua_profile.xml"; + + private Context mContext; + private boolean mLoaded; + + private String mUserAgent; + private String mUAProfUrl; + + DefaultUserAgentInfoLoader(final Context context) { + mContext = context; + } + + @Override + public String getUserAgent() { + load(); + return mUserAgent; + } + + @Override + public String getUAProfUrl() { + load(); + return mUAProfUrl; + } + + private void load() { + if (mLoaded) { + return; + } + boolean didLoad = false; + synchronized (this) { + if (!mLoaded) { + loadLocked(); + mLoaded = true; + didLoad = true; + } + } + if (didLoad) { + Log.i(MmsService.TAG, "Loaded user agent info: " + + "UA=" + mUserAgent + ", UAProfUrl=" + mUAProfUrl); + } + } + + private void loadLocked() { + if (Utils.hasUserAgentApi()) { + // load the MMS User agent and UaProfUrl from TelephonyManager APIs + final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService( + Context.TELEPHONY_SERVICE); + mUserAgent = telephonyManager.getMmsUserAgent(); + mUAProfUrl = telephonyManager.getMmsUAProfUrl(); + } + if (TextUtils.isEmpty(mUserAgent)) { + mUserAgent = DEFAULT_USER_AGENT; + } + if (TextUtils.isEmpty(mUAProfUrl)) { + mUAProfUrl = DEFAULT_UA_PROF_URL; + } + } +} diff --git a/src/android/support/v7/mms/DownloadRequest.java b/src/android/support/v7/mms/DownloadRequest.java new file mode 100644 index 0000000..254c8ca --- /dev/null +++ b/src/android/support/v7/mms/DownloadRequest.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; + +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * Request to download an MMS + */ +class DownloadRequest extends MmsRequest { + + DownloadRequest(final String locationUrl, final Uri pduUri, + final PendingIntent sentIntent) { + super(locationUrl, pduUri, sentIntent); + } + + @Override + protected boolean loadRequest(final Context context, final Bundle mmsConfig) { + // No need to load PDU from app. Always true. + return true; + } + + @Override + protected boolean transferResponse(Context context, Intent fillIn, byte[] response) { + return writePduToContentUri(context, mPduUri, response); + } + + @Override + protected byte[] doHttp(Context context, MmsNetworkManager netMgr, ApnSettingsLoader.Apn apn, + Bundle mmsConfig, String userAgent, String uaProfUrl) throws MmsHttpException { + final MmsHttpClient httpClient = netMgr.getHttpClient(); + return httpClient.execute(getHttpRequestUrl(apn), null/*pdu*/, MmsHttpClient.METHOD_GET, + !TextUtils.isEmpty(apn.getMmsProxy()), apn.getMmsProxy(), apn.getMmsProxyPort(), + mmsConfig, userAgent, uaProfUrl); + + } + + @Override + protected String getHttpRequestUrl(final ApnSettingsLoader.Apn apn) { + return mLocationUrl; + } + + /** + * Write pdu bytes to content provider uri + * + * @param contentUri content provider uri to which bytes should be written + * @param pdu Bytes to write + * @return true if all bytes successfully written else false + */ + public boolean writePduToContentUri(final Context context, final Uri contentUri, + final byte[] pdu) { + if (contentUri == null || pdu == null) { + return false; + } + final Callable<Boolean> copyDownloadedPduToOutput = new Callable<Boolean>() { + public Boolean call() { + ParcelFileDescriptor.AutoCloseOutputStream outStream = null; + try { + final ContentResolver cr = context.getContentResolver(); + final ParcelFileDescriptor pduFd = cr.openFileDescriptor(contentUri, "w"); + outStream = new ParcelFileDescriptor.AutoCloseOutputStream(pduFd); + outStream.write(pdu); + return true; + } catch (IOException e) { + Log.e(MmsService.TAG, "Writing PDU to downloader: IO exception", e); + return false; + } finally { + if (outStream != null) { + try { + outStream.close(); + } catch (IOException ex) { + // Ignore + } + } + } + } + }; + final Future<Boolean> pendingResult = + mPduTransferExecutor.submit(copyDownloadedPduToOutput); + try { + return pendingResult.get(TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (Exception e) { + // Typically a timeout occurred - cancel task + pendingResult.cancel(true); + } + return false; + } + + public static final Parcelable.Creator<DownloadRequest> CREATOR + = new Parcelable.Creator<DownloadRequest>() { + public DownloadRequest createFromParcel(Parcel in) { + return new DownloadRequest(in); + } + + public DownloadRequest[] newArray(int size) { + return new DownloadRequest[size]; + } + }; + + private DownloadRequest(Parcel in) { + super(in); + } +} diff --git a/src/android/support/v7/mms/MmsHttpClient.java b/src/android/support/v7/mms/MmsHttpClient.java new file mode 100644 index 0000000..8ca34da --- /dev/null +++ b/src/android/support/v7/mms/MmsHttpClient.java @@ -0,0 +1,523 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.content.Context; +import android.os.Bundle; +import android.telephony.SmsManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.URL; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * MMS HTTP client for sending and downloading MMS messages + */ +public class MmsHttpClient { + static final String METHOD_POST = "POST"; + static final String METHOD_GET = "GET"; + + private static final String HEADER_CONTENT_TYPE = "Content-Type"; + private static final String HEADER_ACCEPT = "Accept"; + private static final String HEADER_ACCEPT_LANGUAGE = "Accept-Language"; + private static final String HEADER_USER_AGENT = "User-Agent"; + + // The "Accept" header value + private static final String HEADER_VALUE_ACCEPT = + "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic"; + // The "Content-Type" header value + private static final String HEADER_VALUE_CONTENT_TYPE_WITH_CHARSET = + "application/vnd.wap.mms-message; charset=utf-8"; + private static final String HEADER_VALUE_CONTENT_TYPE_WITHOUT_CHARSET = + "application/vnd.wap.mms-message"; + + /* + * Macro names + */ + // The raw phone number + private static final String MACRO_LINE1 = "LINE1"; + // The phone number without country code + private static final String MACRO_LINE1NOCOUNTRYCODE = "LINE1NOCOUNTRYCODE"; + // NAI (Network Access Identifier) + private static final String MACRO_NAI = "NAI"; + + // The possible NAI system property name + private static final String NAI_PROPERTY = "persist.radio.cdma.nai"; + + private final Context mContext; + private final TelephonyManager mTelephonyManager; + + /** + * Constructor + * + * @param context The Context object + */ + MmsHttpClient(Context context) { + mContext = context; + mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + } + + /** + * Execute an MMS HTTP request, either a POST (sending) or a GET (downloading) + * + * @param urlString The request URL, for sending it is usually the MMSC, and for downloading + * it is the message URL + * @param pdu For POST (sending) only, the PDU to send + * @param method HTTP method, POST for sending and GET for downloading + * @param isProxySet Is there a proxy for the MMSC + * @param proxyHost The proxy host + * @param proxyPort The proxy port + * @param mmsConfig The MMS config to use + * @param userAgent The user agent header value + * @param uaProfUrl The UA Prof URL header value + * @return The HTTP response body + * @throws MmsHttpException For any failures + */ + public byte[] execute(String urlString, byte[] pdu, String method, boolean isProxySet, + String proxyHost, int proxyPort, Bundle mmsConfig, String userAgent, String uaProfUrl) + throws MmsHttpException { + Log.d(MmsService.TAG, "HTTP: " + method + " " + Utils.redactUrlForNonVerbose(urlString) + + (isProxySet ? (", proxy=" + proxyHost + ":" + proxyPort) : "") + + ", PDU size=" + (pdu != null ? pdu.length : 0)); + checkMethod(method); + HttpURLConnection connection = null; + try { + Proxy proxy = Proxy.NO_PROXY; + if (isProxySet) { + proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); + } + final URL url = new URL(urlString); + // Now get the connection + connection = (HttpURLConnection) url.openConnection(proxy); + connection.setDoInput(true); + connection.setConnectTimeout( + mmsConfig.getInt(CarrierConfigValuesLoader.CONFIG_HTTP_SOCKET_TIMEOUT, + CarrierConfigValuesLoader.CONFIG_HTTP_SOCKET_TIMEOUT_DEFAULT)); + // ------- COMMON HEADERS --------- + // Header: Accept + connection.setRequestProperty(HEADER_ACCEPT, HEADER_VALUE_ACCEPT); + // Header: Accept-Language + connection.setRequestProperty( + HEADER_ACCEPT_LANGUAGE, getCurrentAcceptLanguage(Locale.getDefault())); + // Header: User-Agent + Log.i(MmsService.TAG, "HTTP: User-Agent=" + userAgent); + connection.setRequestProperty(HEADER_USER_AGENT, userAgent); + // Header: x-wap-profile + final String uaProfUrlTagName = mmsConfig.getString( + CarrierConfigValuesLoader.CONFIG_UA_PROF_TAG_NAME, + CarrierConfigValuesLoader.CONFIG_UA_PROF_TAG_NAME_DEFAULT); + if (uaProfUrl != null) { + Log.i(MmsService.TAG, "HTTP: UaProfUrl=" + uaProfUrl); + connection.setRequestProperty(uaProfUrlTagName, uaProfUrl); + } + // Add extra headers specified by mms_config.xml's httpparams + addExtraHeaders(connection, mmsConfig); + // Different stuff for GET and POST + if (METHOD_POST.equals(method)) { + if (pdu == null || pdu.length < 1) { + Log.e(MmsService.TAG, "HTTP: empty pdu"); + throw new MmsHttpException(0/*statusCode*/, "Sending empty PDU"); + } + connection.setDoOutput(true); + connection.setRequestMethod(METHOD_POST); + if (mmsConfig.getBoolean( + CarrierConfigValuesLoader.CONFIG_SUPPORT_HTTP_CHARSET_HEADER, + CarrierConfigValuesLoader.CONFIG_SUPPORT_HTTP_CHARSET_HEADER_DEFAULT)) { + connection.setRequestProperty(HEADER_CONTENT_TYPE, + HEADER_VALUE_CONTENT_TYPE_WITH_CHARSET); + } else { + connection.setRequestProperty(HEADER_CONTENT_TYPE, + HEADER_VALUE_CONTENT_TYPE_WITHOUT_CHARSET); + } + if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) { + logHttpHeaders(connection.getRequestProperties()); + } + connection.setFixedLengthStreamingMode(pdu.length); + // Sending request body + final OutputStream out = + new BufferedOutputStream(connection.getOutputStream()); + out.write(pdu); + out.flush(); + out.close(); + } else if (METHOD_GET.equals(method)) { + if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) { + logHttpHeaders(connection.getRequestProperties()); + } + connection.setRequestMethod(METHOD_GET); + } + // Get response + final int responseCode = connection.getResponseCode(); + final String responseMessage = connection.getResponseMessage(); + Log.d(MmsService.TAG, "HTTP: " + responseCode + " " + responseMessage); + if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) { + logHttpHeaders(connection.getHeaderFields()); + } + if (responseCode / 100 != 2) { + throw new MmsHttpException(responseCode, responseMessage); + } + final InputStream in = new BufferedInputStream(connection.getInputStream()); + final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + final byte[] buf = new byte[4096]; + int count = 0; + while ((count = in.read(buf)) > 0) { + byteOut.write(buf, 0, count); + } + in.close(); + final byte[] responseBody = byteOut.toByteArray(); + Log.d(MmsService.TAG, "HTTP: response size=" + + (responseBody != null ? responseBody.length : 0)); + return responseBody; + } catch (MalformedURLException e) { + final String redactedUrl = Utils.redactUrlForNonVerbose(urlString); + Log.e(MmsService.TAG, "HTTP: invalid URL " + redactedUrl, e); + throw new MmsHttpException(0/*statusCode*/, "Invalid URL " + redactedUrl, e); + } catch (ProtocolException e) { + final String redactedUrl = Utils.redactUrlForNonVerbose(urlString); + Log.e(MmsService.TAG, "HTTP: invalid URL protocol " + redactedUrl, e); + throw new MmsHttpException(0/*statusCode*/, "Invalid URL protocol " + redactedUrl, e); + } catch (IOException e) { + Log.e(MmsService.TAG, "HTTP: IO failure", e); + throw new MmsHttpException(0/*statusCode*/, e); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + private static void logHttpHeaders(Map<String, List<String>> headers) { + final StringBuilder sb = new StringBuilder(); + if (headers != null) { + for (Map.Entry<String, List<String>> entry : headers.entrySet()) { + final String key = entry.getKey(); + final List<String> values = entry.getValue(); + if (values != null) { + for (String value : values) { + sb.append(key).append('=').append(value).append('\n'); + } + } + } + Log.v(MmsService.TAG, "HTTP: headers\n" + sb.toString()); + } + } + + private static void checkMethod(String method) throws MmsHttpException { + if (!METHOD_GET.equals(method) && !METHOD_POST.equals(method)) { + throw new MmsHttpException(0/*statusCode*/, "Invalid method " + method); + } + } + + private static final String ACCEPT_LANG_FOR_US_LOCALE = "en-US"; + + /** + * Return the Accept-Language header. Use the current locale plus + * US if we are in a different locale than US. + * This code copied from the browser's WebSettings.java + * + * @return Current AcceptLanguage String. + */ + public static String getCurrentAcceptLanguage(Locale locale) { + final StringBuilder buffer = new StringBuilder(); + addLocaleToHttpAcceptLanguage(buffer, locale); + + if (!Locale.US.equals(locale)) { + if (buffer.length() > 0) { + buffer.append(", "); + } + buffer.append(ACCEPT_LANG_FOR_US_LOCALE); + } + + return buffer.toString(); + } + + /** + * Convert obsolete language codes, including Hebrew/Indonesian/Yiddish, + * to new standard. + */ + private static String convertObsoleteLanguageCodeToNew(String langCode) { + if (langCode == null) { + return null; + } + if ("iw".equals(langCode)) { + // Hebrew + return "he"; + } else if ("in".equals(langCode)) { + // Indonesian + return "id"; + } else if ("ji".equals(langCode)) { + // Yiddish + return "yi"; + } + return langCode; + } + + private static void addLocaleToHttpAcceptLanguage(StringBuilder builder, Locale locale) { + final String language = convertObsoleteLanguageCodeToNew(locale.getLanguage()); + if (language != null) { + builder.append(language); + final String country = locale.getCountry(); + if (country != null) { + builder.append("-"); + builder.append(country); + } + } + } + + private static final Pattern MACRO_P = Pattern.compile("##(\\S+)##"); + /** + * Resolve the macro in HTTP param value text + * For example, "something##LINE1##something" is resolved to "something9139531419something" + * + * @param value The HTTP param value possibly containing macros + * @return The HTTP param with macro resolved to real value + */ + private String resolveMacro(String value, Bundle mmsConfig) { + if (TextUtils.isEmpty(value)) { + return value; + } + final Matcher matcher = MACRO_P.matcher(value); + int nextStart = 0; + StringBuilder replaced = null; + while (matcher.find()) { + if (replaced == null) { + replaced = new StringBuilder(); + } + final int matchedStart = matcher.start(); + if (matchedStart > nextStart) { + replaced.append(value.substring(nextStart, matchedStart)); + } + final String macro = matcher.group(1); + final String macroValue = getHttpParamMacro(macro, mmsConfig); + if (macroValue != null) { + replaced.append(macroValue); + } + nextStart = matcher.end(); + } + if (replaced != null && nextStart < value.length()) { + replaced.append(value.substring(nextStart)); + } + return replaced == null ? value : replaced.toString(); + } + + /** + * Add extra HTTP headers from mms_config.xml's httpParams, which is a list of key/value + * pairs separated by "|". Each key/value pair is separated by ":". Value may contain + * macros like "##LINE1##" or "##NAI##" which is resolved with methods in this class + * + * @param connection The HttpURLConnection that we add headers to + * @param mmsConfig The MmsConfig object + */ + private void addExtraHeaders(HttpURLConnection connection, Bundle mmsConfig) { + final String extraHttpParams = mmsConfig.getString( + CarrierConfigValuesLoader.CONFIG_HTTP_PARAMS); + if (!TextUtils.isEmpty(extraHttpParams)) { + // Parse the parameter list + String paramList[] = extraHttpParams.split("\\|"); + for (String paramPair : paramList) { + String splitPair[] = paramPair.split(":", 2); + if (splitPair.length == 2) { + final String name = splitPair[0].trim(); + final String value = resolveMacro(splitPair[1].trim(), mmsConfig); + if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(value)) { + // Add the header if the param is valid + connection.setRequestProperty(name, value); + } + } + } + } + } + + /** + * Return the HTTP param macro value. + * Example: LINE1 returns the phone number, etc. + * + * @param macro The macro name + * @param mmsConfig The carrier configuration values + * @return The value of the defined macro + */ + private String getHttpParamMacro(final String macro, final Bundle mmsConfig) { + if (MACRO_LINE1.equals(macro)) { + return getSelfNumber(); + } else if (MACRO_LINE1NOCOUNTRYCODE.equals(macro)) { + return PhoneNumberHelper.getNumberNoCountryCode( + getSelfNumber(), getSimOrLocaleCountry()); + } else if (MACRO_NAI.equals(macro)) { + return getEncodedNai(mmsConfig.getString( + CarrierConfigValuesLoader.CONFIG_NAI_SUFFIX, + CarrierConfigValuesLoader.CONFIG_NAI_SUFFIX_DEFAULT)); + } + return null; + } + + /** + * Get the device phone number + * + * @return the phone number text + */ + private String getSelfNumber() { + if (Utils.supportMSim()) { + final SubscriptionManager subscriptionManager = SubscriptionManager.from(mContext); + final SubscriptionInfo info = subscriptionManager.getActiveSubscriptionInfo( + SmsManager.getDefaultSmsSubscriptionId()); + if (info != null) { + return info.getNumber(); + } else { + return null; + } + } else { + return mTelephonyManager.getLine1Number(); + } + } + + /** + * Get the country ISO code from SIM or system locale + * + * @return the country ISO code + */ + private String getSimOrLocaleCountry() { + String country = null; + if (Utils.supportMSim()) { + final SubscriptionManager subscriptionManager = SubscriptionManager.from(mContext); + final SubscriptionInfo info = subscriptionManager.getActiveSubscriptionInfo( + SmsManager.getDefaultSmsSubscriptionId()); + if (info != null) { + country = info.getCountryIso(); + } + } else { + country = mTelephonyManager.getSimCountryIso(); + } + if (!TextUtils.isEmpty(country)) { + return country.toUpperCase(); + } else { + return Locale.getDefault().getCountry(); + } + } + + /** + * Get encoded NAI string to use as the HTTP header for some carriers. + * On L-MR1+, we call the hidden system API to get this + * On L-MR1-, we try to find it via system property. + * + * @param naiSuffix the suffix to append to NAI before encoding + * @return the Base64 encoded NAI string to use as HTTP header + */ + private String getEncodedNai(final String naiSuffix) { + String nai; + if (Utils.supportMSim()) { + nai = getNaiBySystemApi( + getSlotId(Utils.getEffectiveSubscriptionId(MmsManager.DEFAULT_SUB_ID))); + } else { + nai = getNaiBySystemProperty(); + } + if (!TextUtils.isEmpty(nai)) { + Log.i(MmsService.TAG, "NAI is not empty"); + if (!TextUtils.isEmpty(naiSuffix)) { + nai = nai + naiSuffix; + } + byte[] encoded = null; + try { + encoded = Base64.encode(nai.getBytes("UTF-8"), Base64.NO_WRAP); + } catch (UnsupportedEncodingException e) { + encoded = Base64.encode(nai.getBytes(), Base64.NO_WRAP); + } + try { + return new String(encoded, "UTF-8"); + } catch (UnsupportedEncodingException e) { + return new String(encoded); + } + } + return null; + } + + /** + * Invoke hidden SubscriptionManager.getSlotId(int) + * + * @param subId the subId + * @return the SIM slot ID + */ + private static int getSlotId(final int subId) { + try { + final Method method = SubscriptionManager.class.getMethod("getSlotId", Integer.TYPE); + if (method != null) { + return (Integer) method.invoke(null, subId); + } + } catch (Exception e) { + Log.w(MmsService.TAG, "SubscriptionManager.getSlotId failed " + e); + } + return -1; + } + + /** + * Get NAI using hidden TelephonyManager.getNai(int) + * + * @param slotId the SIM slot ID + * @return the NAI string + */ + private String getNaiBySystemApi(final int slotId) { + try { + final Method method = mTelephonyManager.getClass().getMethod("getNai", Integer.TYPE); + if (method != null) { + return (String) method.invoke(mTelephonyManager, slotId); + } + } catch (Exception e) { + Log.w(MmsService.TAG, "TelephonyManager.getNai failed " + e); + } + return null; + } + + /** + * Get NAI using hidden SystemProperties.get(String) + * + * @return the NAI string as system property + */ + private static String getNaiBySystemProperty() { + try { + final Class systemPropertiesClass = Class.forName("android.os.SystemProperties"); + if (systemPropertiesClass != null) { + final Method method = systemPropertiesClass.getMethod("get", String.class); + if (method != null) { + return (String) method.invoke(null, NAI_PROPERTY); + } + } + } catch (Exception e) { + Log.w(MmsService.TAG, "SystemProperties.get failed " + e); + } + return null; + } +} diff --git a/src/android/support/v7/mms/MmsHttpException.java b/src/android/support/v7/mms/MmsHttpException.java new file mode 100644 index 0000000..913ba55 --- /dev/null +++ b/src/android/support/v7/mms/MmsHttpException.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +/** + * HTTP exception + */ +public class MmsHttpException extends Exception { + // Optional HTTP status code. 0 means ignore. Otherwise this + // should be a valid HTTP status code. + private final int mStatusCode; + + public MmsHttpException(int statusCode) { + super(); + mStatusCode = statusCode; + } + + public MmsHttpException(int statusCode, String message) { + super(message); + mStatusCode = statusCode; + } + + public MmsHttpException(int statusCode, Throwable cause) { + super(cause); + mStatusCode = statusCode; + } + + public MmsHttpException(int statusCode, String message, Throwable cause) { + super(message, cause); + mStatusCode = statusCode; + } + + public int getStatusCode() { + return mStatusCode; + } +} diff --git a/src/android/support/v7/mms/MmsManager.java b/src/android/support/v7/mms/MmsManager.java new file mode 100644 index 0000000..f9c0a91 --- /dev/null +++ b/src/android/support/v7/mms/MmsManager.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.app.PendingIntent; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.telephony.SmsManager; +import android.util.SparseArray; + +/** + * The public interface of MMS library + */ +public class MmsManager { + /** + * Default subscription ID + */ + public static final int DEFAULT_SUB_ID = -1; + + // Whether to force legacy MMS sending + private static volatile boolean sForceLegacyMms = false; + + // Cached computed overrides for carrier configuration values + private static SparseArray<Bundle> sConfigOverridesMap = new SparseArray<>(); + + /** + * Set the flag about whether to force to use legacy system APIs instead of system MMS API + * + * @param forceLegacyMms value to set + */ + public static void setForceLegacyMms(boolean forceLegacyMms) { + sForceLegacyMms = forceLegacyMms; + } + + /** + * Set the size of thread pool for request execution. + * + * Default is 4 + * + * Note: if system MMS API is used, this has no effect + * + * @param size thread pool size + */ + public static void setThreadPoolSize(int size) { + MmsService.setThreadPoolSize(size); + } + + /** + * Set whether to use wake lock while sending or downloading MMS. + * + * Default value is true + * + * Note: if system MMS API is used, this has no effect + * + * @param useWakeLock true to use wake lock, false otherwise + */ + public static void setUseWakeLock(final boolean useWakeLock) { + MmsService.setUseWakeLock(useWakeLock); + } + + /** + * Set the optional carrier config values loader + * + * Note: if system MMS API is used, this is used to compute the overrides + * of carrier configuration values + * + * @param loader the carrier config values loader + */ + public static void setCarrierConfigValuesLoader(CarrierConfigValuesLoader loader) { + if (loader == null) { + throw new IllegalArgumentException("Carrier configuration loader can not be empty"); + } + synchronized (sConfigOverridesMap) { + MmsService.setCarrierConfigValuesLoader(loader); + sConfigOverridesMap.clear(); + } + } + + /** + * Set the optional APN settings loader + * + * Note: if system MMS API is used, this has no effect + * + * @param loader the APN settings loader + */ + public static void setApnSettingsLoader(ApnSettingsLoader loader) { + if (loader == null) { + throw new IllegalArgumentException("APN settings loader can not be empty"); + } + MmsService.setApnSettingsLoader(loader); + } + + /** + * Set user agent info loader + * + * Note: if system MMS API is used, this is used to compute the overrides + * of carrier configuration values + + * @param loader the user agent info loader + */ + public static void setUserAgentInfoLoader(final UserAgentInfoLoader loader) { + if (loader == null) { + throw new IllegalArgumentException("User agent info loader can not be empty"); + } + synchronized (sConfigOverridesMap) { + MmsService.setUserAgentInfoLoader(loader); + sConfigOverridesMap.clear(); + } + } + + /** + * Send MMS via platform MMS API (if platform supports and not forced to + * use legacy APIs) or legacy APIs + * + * @param subId the subscription ID of the SIM to use + * @param context the Context to use + * @param contentUri the content URI of the PDU to be sent + * @param locationUrl the optional location URL to use for sending + * @param sentIntent the pending intent for returning results + */ + public static void sendMultimediaMessage(int subId, Context context, Uri contentUri, + String locationUrl, PendingIntent sentIntent) { + if (Utils.hasMmsApi() && !sForceLegacyMms) { + subId = Utils.getEffectiveSubscriptionId(subId); + final SmsManager smsManager = Utils.getSmsManager(subId); + smsManager.sendMultimediaMessage(context, contentUri, locationUrl, + getConfigOverrides(subId), sentIntent); + } else { + MmsService.startRequest(context, new SendRequest(locationUrl, contentUri, sentIntent)); + } + } + + /** + * Download MMS via platform MMS API (if platform supports and not forced to + * use legacy APIs) or legacy APIs + * + * @param subId the subscription ID of the SIM to use + * @param context the Context to use + * @param contentUri the content URI of the PDU to be sent + * @param locationUrl the optional location URL to use for sending + * @param downloadedIntent the pending intent for returning results + */ + public static void downloadMultimediaMessage(int subId, Context context, String locationUrl, + Uri contentUri, PendingIntent downloadedIntent) { + if (Utils.hasMmsApi() && !sForceLegacyMms) { + subId = Utils.getEffectiveSubscriptionId(subId); + final SmsManager smsManager = Utils.getSmsManager(subId); + smsManager.downloadMultimediaMessage(context, locationUrl, contentUri, + getConfigOverrides(subId), downloadedIntent); + } else { + MmsService.startRequest(context, + new DownloadRequest(locationUrl, contentUri, downloadedIntent)); + } + } + + /** + * Get carrier configuration values overrides when platform MMS API is called. + * We only need to compute this if customized carrier config values loader or + * user agent info loader are set + * + * @param subId the ID of the SIM to use + * @return a Bundle containing the overrides + */ + private static Bundle getConfigOverrides(final int subId) { + if (!Utils.hasMmsApi()) { + // If MMS API is not present, it is not necessary to compute overrides + return null; + } + Bundle overrides = null; + synchronized (sConfigOverridesMap) { + overrides = sConfigOverridesMap.get(subId); + if (overrides == null) { + overrides = new Bundle(); + sConfigOverridesMap.put(subId, overrides); + computeOverridesLocked(subId, overrides); + } + } + return overrides; + } + + /** + * Compute the overrides, incorporating the user agent info + * + * @param subId the subId of the SIM to use + * @param overrides the computed values overrides + */ + private static void computeOverridesLocked(final int subId, final Bundle overrides) { + // Overrides not computed yet + final CarrierConfigValuesLoader carrierConfigValuesLoader = + MmsService.getCarrierConfigValuesLoader(); + if (carrierConfigValuesLoader != null && + !(carrierConfigValuesLoader instanceof DefaultCarrierConfigValuesLoader)) { + // Compute the overrides for carrier config values first if the config loader + // is not the default one. + final Bundle systemValues = Utils.getSmsManager(subId).getCarrierConfigValues(); + final Bundle callerValues = + MmsService.getCarrierConfigValuesLoader().get(subId); + if (systemValues != null && callerValues != null) { + computeConfigDelta(systemValues, callerValues, overrides); + } else if (systemValues == null && callerValues != null) { + overrides.putAll(callerValues); + } + } + final UserAgentInfoLoader userAgentInfoLoader = MmsService.getUserAgentInfoLoader(); + if (userAgentInfoLoader != null && + !(userAgentInfoLoader instanceof DefaultUserAgentInfoLoader)) { + // Also set the user agent and ua prof url via the overrides + // if the user agent loader is not the default one. + overrides.putString(UserAgentInfoLoader.CONFIG_USER_AGENT, + userAgentInfoLoader.getUserAgent()); + overrides.putString(UserAgentInfoLoader.CONFIG_UA_PROF_URL, + userAgentInfoLoader.getUAProfUrl()); + } + } + + /** + * Compute the delta between two sets of carrier configuration values: system and caller + * + * @param systemValues the system config values + * @param callerValues the caller's config values + * @param delta the delta of values (caller - system), using caller value to override system's + */ + private static void computeConfigDelta(final Bundle systemValues, final Bundle callerValues, + final Bundle delta) { + for (final String key : callerValues.keySet()) { + final Object callerValue = callerValues.get(key); + final Object systemValue = systemValues.get(key); + if ((callerValue != null && systemValue != null && !callerValue.equals(systemValue)) || + (callerValue != null && systemValue == null) || + (callerValue == null && systemValue != null)) { + if (callerValue == null || callerValue instanceof String) { + delta.putString(key, (String) callerValue); + } else if (callerValue instanceof Integer) { + delta.putInt(key, (Integer) callerValue); + } else if (callerValue instanceof Boolean) { + delta.putBoolean(key, (Boolean) callerValue); + } + } + } + } +} diff --git a/src/android/support/v7/mms/MmsNetworkException.java b/src/android/support/v7/mms/MmsNetworkException.java new file mode 100644 index 0000000..776d8ee --- /dev/null +++ b/src/android/support/v7/mms/MmsNetworkException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +/** + * MMS network exception + */ +class MmsNetworkException extends Exception { + + public MmsNetworkException() { + super(); + } + + public MmsNetworkException(String message) { + super(message); + } + + public MmsNetworkException(Throwable cause) { + super(cause); + } + + public MmsNetworkException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/android/support/v7/mms/MmsNetworkManager.java b/src/android/support/v7/mms/MmsNetworkManager.java new file mode 100644 index 0000000..0630293 --- /dev/null +++ b/src/android/support/v7/mms/MmsNetworkManager.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Build; +import android.os.SystemClock; +import android.util.Log; + +import java.lang.reflect.Method; +import java.util.Timer; +import java.util.TimerTask; + +/** + * Class manages MMS network connectivity using legacy platform APIs + * (deprecated since Android L) on pre-L devices (or when forced to + * be used on L and later) + */ +class MmsNetworkManager { + // Hidden platform constants + private static final String FEATURE_ENABLE_MMS = "enableMMS"; + private static final String REASON_VOICE_CALL_ENDED = "2GVoiceCallEnded"; + private static final int APN_ALREADY_ACTIVE = 0; + private static final int APN_REQUEST_STARTED = 1; + private static final int APN_TYPE_NOT_AVAILABLE = 2; + private static final int APN_REQUEST_FAILED = 3; + private static final int APN_ALREADY_INACTIVE = 4; + // A map from platform APN constant to text string + private static final String[] APN_RESULT_STRING = new String[]{ + "already active", + "request started", + "type not available", + "request failed", + "already inactive", + "unknown", + }; + + private static final long NETWORK_ACQUIRE_WAIT_INTERVAL_MS = 15000; + private static final long DEFAULT_NETWORK_ACQUIRE_TIMEOUT_MS = 180000; + private static final String MMS_NETWORK_EXTENSION_TIMER = "mms_network_extension_timer"; + private static final long MMS_NETWORK_EXTENSION_TIMER_WAIT_MS = 30000; + + private static volatile long sNetworkAcquireTimeoutMs = DEFAULT_NETWORK_ACQUIRE_TIMEOUT_MS; + + /** + * Set the network acquire timeout + * + * @param timeoutMs timeout in millisecond + */ + static void setNetworkAcquireTimeout(final long timeoutMs) { + sNetworkAcquireTimeoutMs = timeoutMs; + } + + private final Context mContext; + private final ConnectivityManager mConnectivityManager; + + // If the connectivity intent receiver is registered + private boolean mReceiverRegistered; + // Count of requests that are using the MMS network + private int mUseCount; + // Count of requests that are waiting for connectivity (i.e. in acquireNetwork wait loop) + private int mWaitCount; + // Timer to extend the network connectivity + private Timer mExtensionTimer; + + private final MmsHttpClient mHttpClient; + + private final IntentFilter mConnectivityIntentFilter; + private final BroadcastReceiver mConnectivityChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + if (!ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { + return; + } + final int networkType = getConnectivityChangeNetworkType(intent); + if (networkType != ConnectivityManager.TYPE_MOBILE_MMS) { + return; + } + onMmsConnectivityChange(context, intent); + } + }; + + MmsNetworkManager(final Context context) { + mContext = context; + mConnectivityManager = (ConnectivityManager) mContext.getSystemService( + Context.CONNECTIVITY_SERVICE); + mHttpClient = new MmsHttpClient(mContext); + mConnectivityIntentFilter = new IntentFilter(); + mConnectivityIntentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + mUseCount = 0; + mWaitCount = 0; + } + + ConnectivityManager getConnectivityManager() { + return mConnectivityManager; + } + + MmsHttpClient getHttpClient() { + return mHttpClient; + } + + /** + * Synchronously acquire MMS network connectivity + * + * @throws MmsNetworkException If failed permanently or timed out + */ + void acquireNetwork() throws MmsNetworkException { + Log.i(MmsService.TAG, "Acquire MMS network"); + synchronized (this) { + try { + mUseCount++; + mWaitCount++; + if (mWaitCount == 1) { + // Register the receiver for the first waiting request + registerConnectivityChangeReceiverLocked(); + } + long waitMs = sNetworkAcquireTimeoutMs; + final long beginMs = SystemClock.elapsedRealtime(); + do { + if (!isMobileDataEnabled()) { + // Fast fail if mobile data is not enabled + throw new MmsNetworkException("Mobile data is disabled"); + } + // Always try to extend and check the MMS network connectivity + // before we start waiting to make sure we don't miss the change + // of MMS connectivity. As one example, some devices fail to send + // connectivity change intent. So this would make sure we catch + // the state change. + if (extendMmsConnectivityLocked()) { + // Connected + return; + } + try { + wait(Math.min(waitMs, NETWORK_ACQUIRE_WAIT_INTERVAL_MS)); + } catch (final InterruptedException e) { + Log.w(MmsService.TAG, "Unexpected exception", e); + } + // Calculate the remaining time to wait + waitMs = sNetworkAcquireTimeoutMs - (SystemClock.elapsedRealtime() - beginMs); + } while (waitMs > 0); + // Last check + if (extendMmsConnectivityLocked()) { + return; + } else { + // Reaching here means timed out. + throw new MmsNetworkException("Acquiring MMS network timed out"); + } + } finally { + mWaitCount--; + if (mWaitCount == 0) { + // Receiver is used to listen to connectivity change and unblock + // the waiting requests. If nobody's waiting on change, there is + // no need for the receiver. The auto extension timer will try + // to maintain the connectivity periodically. + unregisterConnectivityChangeReceiverLocked(); + } + } + } + } + + /** + * Release MMS network connectivity. This is ref counted. So it only disconnect + * when the ref count is 0. + */ + void releaseNetwork() { + Log.i(MmsService.TAG, "release MMS network"); + synchronized (this) { + mUseCount--; + if (mUseCount == 0) { + stopNetworkExtensionTimerLocked(); + endMmsConnectivity(); + } + } + } + + String getApnName() { + String apnName = null; + final NetworkInfo mmsNetworkInfo = mConnectivityManager.getNetworkInfo( + ConnectivityManager.TYPE_MOBILE_MMS); + if (mmsNetworkInfo != null) { + apnName = mmsNetworkInfo.getExtraInfo(); + } + return apnName; + } + + // Process mobile MMS connectivity change, waking up the waiting request thread + // in certain conditions: + // - Successfully connected + // - Failed permanently + // - Required another kickoff + // We don't initiate connection here but just notifyAll so the waiting request + // would wake up and retry connection before next wait. + private void onMmsConnectivityChange(final Context context, final Intent intent) { + if (mUseCount < 1) { + return; + } + final NetworkInfo mmsNetworkInfo = + mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_MMS); + // Check availability of the mobile network. + if (mmsNetworkInfo != null) { + if (REASON_VOICE_CALL_ENDED.equals(mmsNetworkInfo.getReason())) { + // This is a very specific fix to handle the case where the phone receives an + // incoming call during the time we're trying to setup the mms connection. + // When the call ends, restart the process of mms connectivity. + // Once the waiting request is unblocked, before the next wait, we would start + // MMS network again. + unblockWait(); + } else { + final NetworkInfo.State state = mmsNetworkInfo.getState(); + if (state == NetworkInfo.State.CONNECTED || + (state == NetworkInfo.State.DISCONNECTED && !isMobileDataEnabled())) { + // Unblock the waiting request when we either connected + // OR + // disconnected due to mobile data disabled therefore needs to fast fail + // (on some devices if mobile data disabled and starting MMS would cause + // an immediate state change to disconnected, so causing a tight loop of + // trying and failing) + // Once the waiting request is unblocked, before the next wait, we would + // check mobile data and start MMS network again. So we should catch + // both the success and the fast failure. + unblockWait(); + } + } + } + } + + private void unblockWait() { + synchronized (this) { + notifyAll(); + } + } + + private void startNetworkExtensionTimerLocked() { + if (mExtensionTimer == null) { + mExtensionTimer = new Timer(MMS_NETWORK_EXTENSION_TIMER, true/*daemon*/); + mExtensionTimer.schedule( + new TimerTask() { + @Override + public void run() { + synchronized (this) { + if (mUseCount > 0) { + try { + // Try extending the connectivity + extendMmsConnectivityLocked(); + } catch (final MmsNetworkException e) { + // Ignore the exception + } + } + } + } + }, + MMS_NETWORK_EXTENSION_TIMER_WAIT_MS); + } + } + + private void stopNetworkExtensionTimerLocked() { + if (mExtensionTimer != null) { + mExtensionTimer.cancel(); + mExtensionTimer = null; + } + } + + private boolean extendMmsConnectivityLocked() throws MmsNetworkException { + final int result = startMmsConnectivity(); + if (result == APN_ALREADY_ACTIVE) { + // Already active + startNetworkExtensionTimerLocked(); + return true; + } else if (result != APN_REQUEST_STARTED) { + stopNetworkExtensionTimerLocked(); + throw new MmsNetworkException("Cannot acquire MMS network: " + + result + " - " + getMmsConnectivityResultString(result)); + } + return false; + } + + private int startMmsConnectivity() { + Log.i(MmsService.TAG, "Start MMS connectivity"); + try { + final Method method = mConnectivityManager.getClass().getMethod( + "startUsingNetworkFeature", Integer.TYPE, String.class); + if (method != null) { + return (Integer) method.invoke( + mConnectivityManager, ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS); + } + } catch (final Exception e) { + Log.w(MmsService.TAG, "ConnectivityManager.startUsingNetworkFeature failed " + e); + } + return APN_REQUEST_FAILED; + } + + private void endMmsConnectivity() { + Log.i(MmsService.TAG, "End MMS connectivity"); + try { + final Method method = mConnectivityManager.getClass().getMethod( + "stopUsingNetworkFeature", Integer.TYPE, String.class); + if (method != null) { + method.invoke( + mConnectivityManager, ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS); + } + } catch (final Exception e) { + Log.w(MmsService.TAG, "ConnectivityManager.stopUsingNetworkFeature failed " + e); + } + } + + private void registerConnectivityChangeReceiverLocked() { + if (!mReceiverRegistered) { + mContext.registerReceiver(mConnectivityChangeReceiver, mConnectivityIntentFilter); + mReceiverRegistered = true; + } + } + + private void unregisterConnectivityChangeReceiverLocked() { + if (mReceiverRegistered) { + mContext.unregisterReceiver(mConnectivityChangeReceiver); + mReceiverRegistered = false; + } + } + + /** + * The absence of a connection type. + */ + private static final int TYPE_NONE = -1; + + /** + * Get the network type of the connectivity change + * + * @param intent the broadcast intent of connectivity change + * @return The change's network type + */ + private static int getConnectivityChangeNetworkType(final Intent intent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, TYPE_NONE); + } else { + final NetworkInfo info = intent.getParcelableExtra( + ConnectivityManager.EXTRA_NETWORK_INFO); + if (info != null) { + return info.getType(); + } + } + return TYPE_NONE; + } + + private static String getMmsConnectivityResultString(int result) { + if (result < 0 || result >= APN_RESULT_STRING.length) { + result = APN_RESULT_STRING.length - 1; + } + return APN_RESULT_STRING[result]; + } + + private boolean isMobileDataEnabled() { + try { + final Class cmClass = mConnectivityManager.getClass(); + final Method method = cmClass.getDeclaredMethod("getMobileDataEnabled"); + method.setAccessible(true); // Make the method callable + // get the setting for "mobile data" + return (Boolean) method.invoke(mConnectivityManager); + } catch (final Exception e) { + Log.w(MmsService.TAG, "TelephonyManager.getMobileDataEnabled failed", e); + } + return false; + } +} diff --git a/src/android/support/v7/mms/MmsRequest.java b/src/android/support/v7/mms/MmsRequest.java new file mode 100644 index 0000000..edf3606 --- /dev/null +++ b/src/android/support/v7/mms/MmsRequest.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v7.mms.pdu.GenericPdu; +import android.support.v7.mms.pdu.PduHeaders; +import android.support.v7.mms.pdu.PduParser; +import android.support.v7.mms.pdu.SendConf; +import android.telephony.SmsManager; +import android.text.TextUtils; +import android.util.Log; + +import java.lang.reflect.Method; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * MMS request base class. This handles the execution of any MMS request. + */ +abstract class MmsRequest implements Parcelable { + /** + * Prepare to make the HTTP request - will download message for sending + * + * @param context the Context + * @param mmsConfig carrier config values to use + * @return true if loading request PDU from calling app succeeds, false otherwise + */ + protected abstract boolean loadRequest(Context context, Bundle mmsConfig); + + /** + * Transfer the received response to the caller + * + * @param context the Context + * @param fillIn the content of pending intent to be returned + * @param response the pdu to transfer + * @return true if transferring response PDU to calling app succeeds, false otherwise + */ + protected abstract boolean transferResponse(Context context, Intent fillIn, byte[] response); + + /** + * Making the HTTP request to MMSC + * + * @param context The context + * @param netMgr The current {@link MmsNetworkManager} + * @param apn The APN + * @param mmsConfig The carrier configuration values to use + * @param userAgent The User-Agent header value + * @param uaProfUrl The UA Prof URL header value + * @return The HTTP response data + * @throws MmsHttpException If any network error happens + */ + protected abstract byte[] doHttp(Context context, MmsNetworkManager netMgr, + ApnSettingsLoader.Apn apn, Bundle mmsConfig, String userAgent, String uaProfUrl) + throws MmsHttpException; + + /** + * Get the HTTP request URL for this MMS request + * + * @param apn The APN to use + * @return The HTTP request URL in text + */ + protected abstract String getHttpRequestUrl(ApnSettingsLoader.Apn apn); + + // Maximum time to spend waiting to read data from a content provider before failing with error. + protected static final int TASK_TIMEOUT_MS = 30 * 1000; + + protected final String mLocationUrl; + protected final Uri mPduUri; + protected final PendingIntent mPendingIntent; + // Thread pool for transferring PDU with MMS apps + protected final ExecutorService mPduTransferExecutor = Executors.newCachedThreadPool(); + + // Whether this request should acquire wake lock + private boolean mUseWakeLock; + + protected MmsRequest(final String locationUrl, final Uri pduUri, + final PendingIntent pendingIntent) { + mLocationUrl = locationUrl; + mPduUri = pduUri; + mPendingIntent = pendingIntent; + mUseWakeLock = true; + } + + void setUseWakeLock(final boolean useWakeLock) { + mUseWakeLock = useWakeLock; + } + + boolean getUseWakeLock() { + return mUseWakeLock; + } + + /** + * Run the MMS request. + * + * @param context the context to use + * @param networkManager the MmsNetworkManager to use to setup MMS network + * @param apnSettingsLoader the APN loader + * @param carrierConfigValuesLoader the carrier config loader + * @param userAgentInfoLoader the user agent info loader + */ + void execute(final Context context, final MmsNetworkManager networkManager, + final ApnSettingsLoader apnSettingsLoader, + final CarrierConfigValuesLoader carrierConfigValuesLoader, + final UserAgentInfoLoader userAgentInfoLoader) { + Log.i(MmsService.TAG, "Execute " + this.getClass().getSimpleName()); + int result = SmsManager.MMS_ERROR_UNSPECIFIED; + int httpStatusCode = 0; + byte[] response = null; + final Bundle mmsConfig = carrierConfigValuesLoader.get(MmsManager.DEFAULT_SUB_ID); + if (mmsConfig == null) { + Log.e(MmsService.TAG, "Failed to load carrier configuration values"); + result = SmsManager.MMS_ERROR_CONFIGURATION_ERROR; + } else if (!loadRequest(context, mmsConfig)) { + Log.e(MmsService.TAG, "Failed to load PDU"); + result = SmsManager.MMS_ERROR_IO_ERROR; + } else { + // Everything's OK. Now execute the request. + try { + // Acquire the MMS network + networkManager.acquireNetwork(); + // Load the potential APNs. In most cases there should be only one APN available. + // On some devices on which we can't obtain APN from system, we look up our own + // APN list. Since we don't have exact information, we may get a list of potential + // APNs to try. Whenever we found a successful APN, we signal it and return. + final String apnName = networkManager.getApnName(); + final List<ApnSettingsLoader.Apn> apns = apnSettingsLoader.get(apnName); + if (apns.size() < 1) { + throw new ApnException("No valid APN"); + } else { + Log.d(MmsService.TAG, "Trying " + apns.size() + " APNs"); + } + final String userAgent = userAgentInfoLoader.getUserAgent(); + final String uaProfUrl = userAgentInfoLoader.getUAProfUrl(); + MmsHttpException lastException = null; + for (ApnSettingsLoader.Apn apn : apns) { + Log.i(MmsService.TAG, "Using APN [" + + "MMSC=" + apn.getMmsc() + ", " + + "PROXY=" + apn.getMmsProxy() + ", " + + "PORT=" + apn.getMmsProxyPort() + "]"); + try { + final String url = getHttpRequestUrl(apn); + // Request a global route for the host to connect + requestRoute(networkManager.getConnectivityManager(), apn, url); + // Perform the HTTP request + response = doHttp( + context, networkManager, apn, mmsConfig, userAgent, uaProfUrl); + // Additional check of whether this is a success + if (isWrongApnResponse(response, mmsConfig)) { + throw new MmsHttpException(0/*statusCode*/, "Invalid sending address"); + } + // Notify APN loader this is a valid APN + apn.setSuccess(); + result = Activity.RESULT_OK; + break; + } catch (MmsHttpException e) { + Log.w(MmsService.TAG, "HTTP or network failure", e); + lastException = e; + } + } + if (lastException != null) { + throw lastException; + } + } catch (ApnException e) { + Log.e(MmsService.TAG, "MmsRequest: APN failure", e); + result = SmsManager.MMS_ERROR_INVALID_APN; + } catch (MmsNetworkException e) { + Log.e(MmsService.TAG, "MmsRequest: MMS network acquiring failure", e); + result = SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS; + } catch (MmsHttpException e) { + Log.e(MmsService.TAG, "MmsRequest: HTTP or network I/O failure", e); + result = SmsManager.MMS_ERROR_HTTP_FAILURE; + httpStatusCode = e.getStatusCode(); + } catch (Exception e) { + Log.e(MmsService.TAG, "MmsRequest: unexpected failure", e); + result = SmsManager.MMS_ERROR_UNSPECIFIED; + } finally { + // Release MMS network + networkManager.releaseNetwork(); + } + } + // Process result and send back via PendingIntent + returnResult(context, result, response, httpStatusCode); + } + + /** + * Check if the response indicates a failure when we send to wrong APN. + * Sometimes even if you send to the wrong APN, a response in valid PDU format can still + * be sent back but with an error status. Check one specific case here. + * + * TODO: maybe there are other possibilities. + * + * @param response the response data + * @param mmsConfig the carrier configuration values to use + * @return false if we find an invalid response case, otherwise true + */ + static boolean isWrongApnResponse(final byte[] response, final Bundle mmsConfig) { + if (response != null && response.length > 0) { + try { + final GenericPdu pdu = new PduParser( + response, + mmsConfig.getBoolean( + CarrierConfigValuesLoader + .CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION, + CarrierConfigValuesLoader + .CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION_DEFAULT)) + .parse(); + if (pdu != null && pdu instanceof SendConf) { + final SendConf sendConf = (SendConf) pdu; + final int responseStatus = sendConf.getResponseStatus(); + return responseStatus == + PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_SENDING_ADDRESS_UNRESOLVED || + responseStatus == + PduHeaders.RESPONSE_STATUS_ERROR_SENDING_ADDRESS_UNRESOLVED; + } + } catch (RuntimeException e) { + Log.w(MmsService.TAG, "Parsing response failed", e); + } + } + return false; + } + + /** + * Return the result back via pending intent + * + * @param context The context + * @param result The result code of execution + * @param response The response body + * @param httpStatusCode The optional http status code in case of http failure + */ + void returnResult(final Context context, int result, final byte[] response, + final int httpStatusCode) { + if (mPendingIntent == null) { + // Result not needed + return; + } + // Extra information to send back with the pending intent + final Intent fillIn = new Intent(); + if (response != null) { + if (!transferResponse(context, fillIn, response)) { + // Failed to send PDU data back to caller + result = SmsManager.MMS_ERROR_IO_ERROR; + } + } + if (result == SmsManager.MMS_ERROR_HTTP_FAILURE && httpStatusCode != 0) { + // For HTTP failure, fill in the status code for more information + fillIn.putExtra(SmsManager.EXTRA_MMS_HTTP_STATUS, httpStatusCode); + } + try { + mPendingIntent.send(context, result, fillIn); + } catch (PendingIntent.CanceledException e) { + Log.e(MmsService.TAG, "Sending pending intent canceled", e); + } + } + + /** + * Request the route to the APN (either proxy host or the MMSC host) + * + * @param connectivityManager the ConnectivityManager to use + * @param apn the current APN + * @param url the URL to connect to + * @throws MmsHttpException for unknown host or route failure + */ + private static void requestRoute(final ConnectivityManager connectivityManager, + final ApnSettingsLoader.Apn apn, final String url) throws MmsHttpException { + String host = apn.getMmsProxy(); + if (TextUtils.isEmpty(host)) { + final Uri uri = Uri.parse(url); + host = uri.getHost(); + } + boolean success = false; + // Request route to all resolved host addresses + try { + for (final InetAddress addr : InetAddress.getAllByName(host)) { + final boolean requested = requestRouteToHostAddress(connectivityManager, addr); + if (requested) { + success = true; + Log.i(MmsService.TAG, "Requested route to " + addr); + } else { + Log.i(MmsService.TAG, "Could not requested route to " + addr); + } + } + if (!success) { + throw new MmsHttpException(0/*statusCode*/, "No route requested"); + } + } catch (UnknownHostException e) { + Log.w(MmsService.TAG, "Unknown host " + host); + throw new MmsHttpException(0/*statusCode*/, "Unknown host"); + } + } + + private static final Integer TYPE_MOBILE_MMS = + Integer.valueOf(ConnectivityManager.TYPE_MOBILE_MMS); + /** + * Wrapper for platform API requestRouteToHostAddress + * + * We first try the hidden but correct method on ConnectivityManager. If we can't, use + * the old but buggy one + * + * @param connMgr the ConnectivityManager instance + * @param inetAddr the InetAddress to request + * @return true if route is successfully setup, false otherwise + */ + private static boolean requestRouteToHostAddress(final ConnectivityManager connMgr, + final InetAddress inetAddr) { + // First try the good method using reflection + try { + final Method method = connMgr.getClass().getMethod("requestRouteToHostAddress", + Integer.TYPE, InetAddress.class); + if (method != null) { + return (Boolean) method.invoke(connMgr, TYPE_MOBILE_MMS, inetAddr); + } + } catch (Exception e) { + Log.w(MmsService.TAG, "ConnectivityManager.requestRouteToHostAddress failed " + e); + } + // If we fail, try the old but buggy one + if (inetAddr instanceof Inet4Address) { + try { + final Method method = connMgr.getClass().getMethod("requestRouteToHost", + Integer.TYPE, Integer.TYPE); + if (method != null) { + return (Boolean) method.invoke(connMgr, TYPE_MOBILE_MMS, + inetAddressToInt(inetAddr)); + } + } catch (Exception e) { + Log.w(MmsService.TAG, "ConnectivityManager.requestRouteToHost failed " + e); + } + } + return false; + } + + /** + * Convert a IPv4 address from an InetAddress to an integer + * + * @param inetAddr is an InetAddress corresponding to the IPv4 address + * @return the IP address as an integer in network byte order + */ + private static int inetAddressToInt(final InetAddress inetAddr) + throws IllegalArgumentException { + final byte [] addr = inetAddr.getAddress(); + return ((addr[3] & 0xff) << 24) | ((addr[2] & 0xff) << 16) | + ((addr[1] & 0xff) << 8) | (addr[0] & 0xff); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeByte((byte) (mUseWakeLock ? 1 : 0)); + parcel.writeString(mLocationUrl); + parcel.writeParcelable(mPduUri, 0); + parcel.writeParcelable(mPendingIntent, 0); + } + + protected MmsRequest(final Parcel in) { + final ClassLoader classLoader = MmsRequest.class.getClassLoader(); + mUseWakeLock = in.readByte() != 0; + mLocationUrl = in.readString(); + mPduUri = in.readParcelable(classLoader); + mPendingIntent = in.readParcelable(classLoader); + } +} diff --git a/src/android/support/v7/mms/MmsService.java b/src/android/support/v7/mms/MmsService.java new file mode 100644 index 0000000..650562f --- /dev/null +++ b/src/android/support/v7/mms/MmsService.java @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.PowerManager; +import android.os.Process; +import android.telephony.SmsManager; +import android.util.Log; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; + +/** + * Service to execute MMS requests using deprecated legacy APIs on older platform (prior to L) + */ +public class MmsService extends Service { + static final String TAG = "MmsLib"; + + //The default number of threads allowed to run MMS requests + private static final int DEFAULT_THREAD_POOL_SIZE = 4; + // Delay before stopping the service + private static final int SERVICE_STOP_DELAY_MILLIS = 2000; + + private static final String EXTRA_REQUEST = "request"; + private static final String EXTRA_MYPID = "mypid"; + + private static final String WAKELOCK_ID = "mmslib_wakelock"; + + /** + * Thread pool size for each request queue + */ + private static volatile int sThreadPoolSize = DEFAULT_THREAD_POOL_SIZE; + + /** + * Optional wake lock to use + */ + private static volatile boolean sUseWakeLock = true; + private static volatile PowerManager.WakeLock sWakeLock = null; + private static final Object sWakeLockLock = new Object(); + + /** + * Carrier configuration values loader + */ + private static volatile CarrierConfigValuesLoader sCarrierConfigValuesLoader = null; + + /** + * APN loader + */ + private static volatile ApnSettingsLoader sApnSettingsLoader = null; + + /** + * UserAgent and UA Prof URL loader + */ + private static volatile UserAgentInfoLoader sUserAgentInfoLoader = null; + + /** + * Set the size of thread pool for request execution. + * Default is DEFAULT_THREAD_POOL_SIZE + * + * @param size thread pool size + */ + static void setThreadPoolSize(final int size) { + sThreadPoolSize = size; + } + + /** + * Set whether to use wake lock + * + * @param useWakeLock true to use wake lock, false otherwise + */ + static void setUseWakeLock(final boolean useWakeLock) { + sUseWakeLock = useWakeLock; + } + + /** + * Set the optional carrier config values + * + * @param loader the carrier config values loader + */ + static void setCarrierConfigValuesLoader(final CarrierConfigValuesLoader loader) { + sCarrierConfigValuesLoader = loader; + } + + /** + * Get the current carrier config values loader + * + * @return the carrier config values loader currently set + */ + static CarrierConfigValuesLoader getCarrierConfigValuesLoader() { + return sCarrierConfigValuesLoader; + } + + /** + * Set APN settings loader + * + * @param loader the APN settings loader + */ + static void setApnSettingsLoader(final ApnSettingsLoader loader) { + sApnSettingsLoader = loader; + } + + /** + * Get the current APN settings loader + * + * @return the APN settings loader currently set + */ + static ApnSettingsLoader getApnSettingsLoader() { + return sApnSettingsLoader; + } + + /** + * Set user agent info loader + * + * @param loader the user agent info loader + */ + static void setUserAgentInfoLoader(final UserAgentInfoLoader loader) { + sUserAgentInfoLoader = loader; + } + + /** + * Get the current user agent info loader + * + * @return the user agent info loader currently set + */ + static UserAgentInfoLoader getUserAgentInfoLoader() { + return sUserAgentInfoLoader; + } + + /** + * Make sure loaders are not null. Set to default if that's the case + * + * @param context the Context to use + */ + private static void ensureLoaders(final Context context) { + if (sUserAgentInfoLoader == null) { + sUserAgentInfoLoader = new DefaultUserAgentInfoLoader(context); + } + if (sCarrierConfigValuesLoader == null) { + sCarrierConfigValuesLoader = new DefaultCarrierConfigValuesLoader(context); + } + if (sApnSettingsLoader == null) { + sApnSettingsLoader = new DefaultApnSettingsLoader(context); + } + } + + /** + * Acquire the wake lock + * + * @param context the context to use + */ + private static void acquireWakeLock(final Context context) { + synchronized (sWakeLockLock) { + if (sWakeLock == null) { + final PowerManager pm = + (PowerManager) context.getSystemService(Context.POWER_SERVICE); + sWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_ID); + } + sWakeLock.acquire(); + } + } + + /** + * Release the wake lock + */ + private static void releaseWakeLock() { + boolean releasedEmptyWakeLock = false; + synchronized (sWakeLockLock) { + if (sWakeLock != null) { + sWakeLock.release(); + } else { + releasedEmptyWakeLock = true; + } + } + if (releasedEmptyWakeLock) { + Log.w(TAG, "Releasing empty wake lock"); + } + } + + /** + * Check if wake lock is not held (e.g. when service stops) + */ + private static void verifyWakeLockNotHeld() { + boolean wakeLockHeld = false; + synchronized (sWakeLockLock) { + wakeLockHeld = sWakeLock != null && sWakeLock.isHeld(); + } + if (wakeLockHeld) { + Log.e(TAG, "Wake lock still held!"); + } + } + + // Remember my PID to discard restarted intent + private static volatile int sMyPid = -1; + + /** + * Get the current PID + * + * @return the current PID + */ + private static int getMyPid() { + if (sMyPid < 0) { + sMyPid = Process.myPid(); + } + return sMyPid; + } + + /** + * Check if the intent is coming from this process + * + * @param intent the incoming intent for the service + * @return true if the intent is from the current process + */ + private static boolean fromThisProcess(final Intent intent) { + final int pid = intent.getIntExtra(EXTRA_MYPID, -1); + return pid == getMyPid(); + } + + // Request execution thread pools. One thread pool for sending and one for downloading. + // The size of the thread pool controls the parallelism of request execution. + // See {@link setThreadPoolSize} + private ExecutorService[] mExecutors = new ExecutorService[2]; + + // Active request count + private int mActiveRequestCount; + // The latest intent startId, used for safely stopping service + private int mLastStartId; + + private MmsNetworkManager mNetworkManager; + + // Handler for scheduling service stop + private final Handler mHandler = new Handler(); + // Service stop task + private final Runnable mServiceStopRunnable = new Runnable() { + @Override + public void run() { + tryStopService(); + } + }; + + /** + * Start the service with a request + * + * @param context the Context to use + * @param request the request to start + */ + public static void startRequest(final Context context, final MmsRequest request) { + final boolean useWakeLock = sUseWakeLock; + request.setUseWakeLock(useWakeLock); + final Intent intent = new Intent(context, MmsService.class); + intent.putExtra(EXTRA_REQUEST, request); + intent.putExtra(EXTRA_MYPID, getMyPid()); + if (useWakeLock) { + acquireWakeLock(context); + } + if (context.startService(intent) == null) { + if (useWakeLock) { + releaseWakeLock(); + } + } + } + + @Override + public void onCreate() { + super.onCreate(); + + ensureLoaders(this); + + for (int i = 0; i < mExecutors.length; i++) { + mExecutors[i] = Executors.newFixedThreadPool(sThreadPoolSize); + } + + mNetworkManager = new MmsNetworkManager(this); + + synchronized (this) { + mActiveRequestCount = 0; + mLastStartId = -1; + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + for (ExecutorService executor : mExecutors) { + executor.shutdown(); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // Always remember the latest startId for use when we try releasing the service + synchronized (this) { + mLastStartId = startId; + } + boolean scheduled = false; + if (intent != null) { + // There is a rare situation that right after a intent is started, + // the service gets killed. Then the service will restart with + // the old intent which we don't want it to run since it will + // break our assumption for wake lock. Check the process ID + // embedded in the intent to make sure it is indeed from the + // the current life of this service. + if (fromThisProcess(intent)) { + final MmsRequest request = intent.getParcelableExtra(EXTRA_REQUEST); + if (request != null) { + try { + retainService(request, new Runnable() { + @Override + public void run() { + try { + request.execute( + MmsService.this, + mNetworkManager, + getApnSettingsLoader(), + getCarrierConfigValuesLoader(), + getUserAgentInfoLoader()); + } catch (Exception e) { + Log.w(TAG, "Unexpected execution failure", e); + } finally { + if (request.getUseWakeLock()) { + releaseWakeLock(); + } + releaseService(); + } + } + }); + scheduled = true; + } catch (RejectedExecutionException e) { + // Rare thing happened. Send back failure using the pending intent + // and also release the wake lock. + Log.w(TAG, "Executing request failed " + e); + request.returnResult(this, SmsManager.MMS_ERROR_UNSPECIFIED, + null/*response*/, 0/*httpStatusCode*/); + if (request.getUseWakeLock()) { + releaseWakeLock(); + } + } + } else { + Log.w(TAG, "Empty request"); + } + } else { + Log.w(TAG, "Got a restarted intent from previous incarnation"); + } + } else { + Log.w(TAG, "Empty intent"); + } + if (!scheduled) { + // If the request is not started successfully, we need to try shutdown the service + // if nobody is using it. + tryScheduleStop(); + } + return START_NOT_STICKY; + } + + /** + * Retain the service for executing the request in service thread pool + * + * @param request The request to execute + * @param runnable The runnable to run the request in thread pool + */ + private void retainService(final MmsRequest request, final Runnable runnable) { + final ExecutorService executor = getRequestExecutor(request); + synchronized (this) { + executor.execute(runnable); + mActiveRequestCount++; + } + } + + /** + * Release the service from the request. If nobody is using it, schedule service stop. + */ + private void releaseService() { + synchronized (this) { + mActiveRequestCount--; + if (mActiveRequestCount <= 0) { + mActiveRequestCount = 0; + rescheduleServiceStop(); + } + } + } + + /** + * Schedule the service stop if there is no active request + */ + private void tryScheduleStop() { + synchronized (this) { + if (mActiveRequestCount == 0) { + rescheduleServiceStop(); + } + } + } + + /** + * Reschedule service stop task + */ + private void rescheduleServiceStop() { + mHandler.removeCallbacks(mServiceStopRunnable); + mHandler.postDelayed(mServiceStopRunnable, SERVICE_STOP_DELAY_MILLIS); + } + + /** + * Really try to stop the service if there is not active request + */ + private void tryStopService() { + Boolean stopped = null; + synchronized (this) { + if (mActiveRequestCount == 0) { + stopped = stopSelfResult(mLastStartId); + } + } + logServiceStop(stopped); + } + + /** + * Log the result of service stopping. Also check wake lock status when service stops. + * + * @param stopped Not empty if service stop is performed: true if really stopped, false + * if cancelled. + */ + private void logServiceStop(final Boolean stopped) { + if (stopped != null) { + if (stopped) { + Log.i(TAG, "Service successfully stopped"); + verifyWakeLockNotHeld(); + } else { + Log.i(TAG, "Service stopping cancelled"); + } + } + } + + private ExecutorService getRequestExecutor(final MmsRequest request) { + if (request instanceof SendRequest) { + // Send + return mExecutors[0]; + } else { + // Download + return mExecutors[1]; + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } +} diff --git a/src/android/support/v7/mms/MmsXmlResourceParser.java b/src/android/support/v7/mms/MmsXmlResourceParser.java new file mode 100644 index 0000000..1ee73e2 --- /dev/null +++ b/src/android/support/v7/mms/MmsXmlResourceParser.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.util.Log; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * Base class for a parser of XML resources + */ +abstract class MmsXmlResourceParser { + /** + * Parse the content + * + * @throws IOException + * @throws XmlPullParserException + */ + protected abstract void parseRecord() throws IOException, XmlPullParserException; + + /** + * Get the root tag of the content + * + * @return the text of root tag + */ + protected abstract String getRootTag(); + + private final StringBuilder mLogStringBuilder = new StringBuilder(); + + protected final XmlPullParser mInputParser; + + protected MmsXmlResourceParser(XmlPullParser parser) { + mInputParser = parser; + } + + void parse() { + try { + // Find the first element + if (advanceToNextEvent(XmlPullParser.START_TAG) != XmlPullParser.START_TAG) { + throw new XmlPullParserException("ApnsXmlProcessor: expecting start tag @" + + xmlParserDebugContext()); + } + if (!getRootTag().equals(mInputParser.getName())) { + Log.w(MmsService.TAG, "Carrier config does not start with " + getRootTag()); + return; + } + // We are at the start tag + for (;;) { + int nextEvent; + // Skipping spaces + while ((nextEvent = mInputParser.next()) == XmlPullParser.TEXT); + if (nextEvent == XmlPullParser.START_TAG) { + // Parse one record + parseRecord(); + } else if (nextEvent == XmlPullParser.END_TAG) { + break; + } else { + throw new XmlPullParserException("Expecting start or end tag @" + + xmlParserDebugContext()); + } + } + } catch (IOException e) { + Log.w(MmsService.TAG, "XmlResourceParser: I/O failure", e); + } catch (XmlPullParserException e) { + Log.w(MmsService.TAG, "XmlResourceParser: parsing failure", e); + } + } + + /** + * Move XML parser forward to next event type or the end of doc + * + * @param eventType + * @return The final event type we meet + * @throws XmlPullParserException + * @throws IOException + */ + protected int advanceToNextEvent(int eventType) throws XmlPullParserException, IOException { + for (;;) { + int nextEvent = mInputParser.next(); + if (nextEvent == eventType + || nextEvent == XmlPullParser.END_DOCUMENT) { + return nextEvent; + } + } + } + + /** + * @return The debugging information of the parser's current position + */ + protected String xmlParserDebugContext() { + mLogStringBuilder.setLength(0); + if (mInputParser != null) { + try { + final int eventType = mInputParser.getEventType(); + mLogStringBuilder.append(xmlParserEventString(eventType)); + if (eventType == XmlPullParser.START_TAG + || eventType == XmlPullParser.END_TAG + || eventType == XmlPullParser.TEXT) { + mLogStringBuilder.append('<').append(mInputParser.getName()); + for (int i = 0; i < mInputParser.getAttributeCount(); i++) { + mLogStringBuilder.append(' ') + .append(mInputParser.getAttributeName(i)) + .append('=') + .append(mInputParser.getAttributeValue(i)); + } + mLogStringBuilder.append("/>"); + } + return mLogStringBuilder.toString(); + } catch (XmlPullParserException e) { + Log.w(MmsService.TAG, "XmlResourceParser exception", e); + } + } + return "Unknown"; + } + + private static String xmlParserEventString(int event) { + switch (event) { + case XmlPullParser.START_DOCUMENT: return "START_DOCUMENT"; + case XmlPullParser.END_DOCUMENT: return "END_DOCUMENT"; + case XmlPullParser.START_TAG: return "START_TAG"; + case XmlPullParser.END_TAG: return "END_TAG"; + case XmlPullParser.TEXT: return "TEXT"; + } + return Integer.toString(event); + } +} diff --git a/src/android/support/v7/mms/PhoneNumberHelper.java b/src/android/support/v7/mms/PhoneNumberHelper.java new file mode 100644 index 0000000..99551c0 --- /dev/null +++ b/src/android/support/v7/mms/PhoneNumberHelper.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.text.TextUtils; +import android.util.Log; + +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.Phonenumber; + +/** + * Helper methods for phone number formatting + * This is isolated into a standalone class since it depends on libphonenumber + */ +public class PhoneNumberHelper { + /** + * Given a phone number, get its national part without country code + * + * @param number the original number + * @param country the country ISO code + * @return the national number + */ + static String getNumberNoCountryCode(final String number, final String country) { + if (!TextUtils.isEmpty(number)) { + final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); + try { + final Phonenumber.PhoneNumber phoneNumber = phoneNumberUtil.parse(number, country); + if (phoneNumber != null && phoneNumberUtil.isValidNumber(phoneNumber)) { + return phoneNumberUtil + .format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL) + .replaceAll("\\D", ""); + } + } catch (final NumberParseException e) { + Log.w(MmsService.TAG, "getNumberNoCountryCode: invalid number " + e); + } + } + return number; + } +} diff --git a/src/android/support/v7/mms/SendRequest.java b/src/android/support/v7/mms/SendRequest.java new file mode 100644 index 0000000..1b7b1db --- /dev/null +++ b/src/android/support/v7/mms/SendRequest.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; +import android.telephony.SmsManager; +import android.text.TextUtils; +import android.util.Log; + +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * Request to send an MMS + */ +class SendRequest extends MmsRequest { + // Max send response PDU size in bytes (exceeding this may cause problem with + // system intent delivery). + private static final int MAX_SEND_RESPONSE_SIZE = 1000 * 1024; + + private byte[] mPduData; + + SendRequest(final String locationUrl, final Uri pduUri, final PendingIntent sentIntent) { + super(locationUrl, pduUri, sentIntent); + } + + @Override + protected boolean loadRequest(final Context context, final Bundle mmsConfig) { + mPduData = readPduFromContentUri( + context, + mPduUri, + mmsConfig.getInt( + CarrierConfigValuesLoader.CONFIG_MAX_MESSAGE_SIZE, + CarrierConfigValuesLoader.CONFIG_MAX_MESSAGE_SIZE_DEFAULT)); + return (mPduData != null); + } + + @Override + protected boolean transferResponse(final Context context, final Intent fillIn, + final byte[] response) { + // SendConf pdus are always small and can be included in the intent + if (response != null && fillIn != null) { + if (response.length > MAX_SEND_RESPONSE_SIZE) { + // If the response PDU is too large, it won't be able to fit in + // the PendingIntent to be transferred via system IPC. + return false; + } + fillIn.putExtra(SmsManager.EXTRA_MMS_DATA, response); + } + return true; + } + + @Override + protected byte[] doHttp(Context context, MmsNetworkManager netMgr, ApnSettingsLoader.Apn apn, + Bundle mmsConfig, String userAgent, String uaProfUrl) throws MmsHttpException { + final MmsHttpClient httpClient = netMgr.getHttpClient(); + return httpClient.execute(getHttpRequestUrl(apn), mPduData, MmsHttpClient.METHOD_POST, + !TextUtils.isEmpty(apn.getMmsProxy()), apn.getMmsProxy(), apn.getMmsProxyPort(), + mmsConfig, userAgent, uaProfUrl); + } + + @Override + protected String getHttpRequestUrl(final ApnSettingsLoader.Apn apn) { + return !TextUtils.isEmpty(mLocationUrl) ? mLocationUrl : apn.getMmsc(); + } + + /** + * Read pdu from content provider uri + * + * @param contentUri content provider uri from which to read + * @param maxSize maximum number of bytes to read + * @return pdu bytes if succeeded else null + */ + public byte[] readPduFromContentUri(final Context context, final Uri contentUri, + final int maxSize) { + if (contentUri == null) { + return null; + } + final Callable<byte[]> copyPduToArray = new Callable<byte[]>() { + public byte[] call() { + ParcelFileDescriptor.AutoCloseInputStream inStream = null; + try { + final ContentResolver cr = context.getContentResolver(); + final ParcelFileDescriptor pduFd = cr.openFileDescriptor(contentUri, "r"); + inStream = new ParcelFileDescriptor.AutoCloseInputStream(pduFd); + // Request one extra byte to make sure file not bigger than maxSize + final byte[] readBuf = new byte[maxSize+1]; + final int bytesRead = inStream.read(readBuf, 0, maxSize+1); + if (bytesRead <= 0) { + Log.e(MmsService.TAG, "Reading PDU from sender: empty PDU"); + return null; + } + if (bytesRead > maxSize) { + Log.e(MmsService.TAG, "Reading PDU from sender: PDU too large"); + return null; + } + // Copy and return the exact length of bytes + final byte[] result = new byte[bytesRead]; + System.arraycopy(readBuf, 0, result, 0, bytesRead); + return result; + } catch (IOException e) { + Log.e(MmsService.TAG, "Reading PDU from sender: IO exception", e); + return null; + } finally { + if (inStream != null) { + try { + inStream.close(); + } catch (IOException ex) { + // Ignore + } + } + } + } + }; + final Future<byte[]> pendingResult = mPduTransferExecutor.submit(copyPduToArray); + try { + return pendingResult.get(TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (Exception e) { + // Typically a timeout occurred - cancel task + pendingResult.cancel(true); + } + return null; + } + + public static final Parcelable.Creator<SendRequest> CREATOR + = new Parcelable.Creator<SendRequest>() { + public SendRequest createFromParcel(Parcel in) { + return new SendRequest(in); + } + + public SendRequest[] newArray(int size) { + return new SendRequest[size]; + } + }; + + private SendRequest(Parcel in) { + super(in); + } +} diff --git a/src/android/support/v7/mms/UserAgentInfoLoader.java b/src/android/support/v7/mms/UserAgentInfoLoader.java new file mode 100644 index 0000000..214d58d --- /dev/null +++ b/src/android/support/v7/mms/UserAgentInfoLoader.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +/** + * Interface to load UserAgent and UA Prof URL + */ +public interface UserAgentInfoLoader { + // Carrier configuration keys for passing as config overrides into system MMS service + public static final String CONFIG_USER_AGENT = "userAgent"; + public static final String CONFIG_UA_PROF_URL = "uaProfUrl"; + + /** + * Get UserAgent value + * + * @return the text of UserAgent + */ + String getUserAgent(); + + /** + * Get UA Profile URL + * + * @return the URL of UA profile + */ + String getUAProfUrl(); +} diff --git a/src/android/support/v7/mms/Utils.java b/src/android/support/v7/mms/Utils.java new file mode 100644 index 0000000..cb55c2e --- /dev/null +++ b/src/android/support/v7/mms/Utils.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms; + +import android.content.Context; +import android.content.res.Configuration; +import android.os.Build; +import android.telephony.SmsManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Utility methods + */ +class Utils { + /** + * Check if MMS API is available + * + * @return true if MMS API is available, false otherwise + */ + static boolean hasMmsApi() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + } + + /** + * Check if support multi-SIM + * + * @return true if MSIM is supported, false otherwise + */ + static boolean supportMSim() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1; + } + + /** + * Check if support APIs for getting UserAgent and UAProfUrl + * + * @return true if those APIs are supported, false otherwise + */ + static boolean hasUserAgentApi() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + } + + /** + * Get system SmsManager + * + * @param subId the subscription ID of the SmsManager + * @return the SmsManager for the input subId + */ + static SmsManager getSmsManager(final int subId) { + if (supportMSim()) { + return SmsManager.getSmsManagerForSubscriptionId(subId); + } else { + return SmsManager.getDefault(); + } + } + + /** + * Get the real subscription ID if the input is -1 + * + * @param subId input subscription ID + * @return the default SMS subscription ID if the input is -1, otherwise the original + */ + static int getEffectiveSubscriptionId(int subId) { + if (supportMSim()) { + if (subId == MmsManager.DEFAULT_SUB_ID) { + subId = SmsManager.getDefaultSmsSubscriptionId(); + } + } + if (subId < 0) { + subId = MmsManager.DEFAULT_SUB_ID; + } + return subId; + } + + /** + * Get MCC/MNC of an SIM subscription + * + * @param context the Context to use + * @param subId the SIM subId + * @return a non-empty array with exactly two elements, first is mcc and last is mnc. + */ + static int[] getMccMnc(final Context context, final int subId) { + final int[] mccMnc = new int[] { 0, 0 }; + if (Utils.supportMSim()) { + final SubscriptionManager subscriptionManager = SubscriptionManager.from(context); + final SubscriptionInfo subInfo = subscriptionManager.getActiveSubscriptionInfo(subId); + if (subInfo != null) { + mccMnc[0] = subInfo.getMcc(); + mccMnc[1] = subInfo.getMnc(); + } + } else { + final TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + final String mccMncString = telephonyManager.getSimOperator(); + try { + mccMnc[0] = Integer.parseInt(mccMncString.substring(0, 3)); + mccMnc[1] = Integer.parseInt(mccMncString.substring(3)); + } catch (Exception e) { + Log.w(MmsService.TAG, "Invalid mcc/mnc from system " + mccMncString + ": " + e); + mccMnc[0] = 0; + mccMnc[1] = 0; + } + } + return mccMnc; + } + + /** + * Get a subscription's Context so we can load resources from it + * + * @param context the sub-independent Context + * @param subId the SIM's subId + * @return the sub-dependent Context + */ + static Context getSubDepContext(final Context context, final int subId) { + if (!supportMSim()) { + return context; + } + final int[] mccMnc = getMccMnc(context, subId); + final int mcc = mccMnc[0]; + final int mnc = mccMnc[1]; + if (mcc == 0 && mnc == 0) { + return context; + } + final Configuration subConfig = new Configuration(); + subConfig.mcc = mcc; + subConfig.mnc = mnc; + return context.createConfigurationContext(subConfig); + } + + /** + * Redact the URL for non-VERBOSE logging. Replace url with only the host part and the length + * of the input URL string. + * + * @param urlString + * @return + */ + static String redactUrlForNonVerbose(String urlString) { + if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) { + // Don't redact for VERBOSE level logging + return urlString; + } + if (TextUtils.isEmpty(urlString)) { + return urlString; + } + String protocol = "http"; + String host = ""; + try { + final URL url = new URL(urlString); + protocol = url.getProtocol(); + host = url.getHost(); + } catch (MalformedURLException e) { + // Ignore + } + // Print "http://host[length]" + final StringBuilder sb = new StringBuilder(); + sb.append(protocol).append("://").append(host) + .append("[").append(urlString.length()).append("]"); + return sb.toString(); + } +} diff --git a/src/android/support/v7/mms/pdu/AcknowledgeInd.java b/src/android/support/v7/mms/pdu/AcknowledgeInd.java new file mode 100644 index 0000000..10f9e35 --- /dev/null +++ b/src/android/support/v7/mms/pdu/AcknowledgeInd.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +/** + * M-Acknowledge.ind PDU. + */ +public class AcknowledgeInd extends GenericPdu { + /** + * Constructor, used when composing a M-Acknowledge.ind pdu. + * + * @param mmsVersion current viersion of mms + * @param transactionId the transaction-id value + * @throws InvalidHeaderValueException if parameters are invalid. + * NullPointerException if transactionId is null. + */ + public AcknowledgeInd(int mmsVersion, byte[] transactionId) + throws InvalidHeaderValueException { + super(); + + setMessageType(PduHeaders.MESSAGE_TYPE_ACKNOWLEDGE_IND); + setMmsVersion(mmsVersion); + setTransactionId(transactionId); + } + + /** + * Constructor with given headers. + * + * @param headers Headers for this PDU. + */ + AcknowledgeInd(PduHeaders headers) { + super(headers); + } + + /** + * Get X-Mms-Report-Allowed field value. + * + * @return the X-Mms-Report-Allowed value + */ + public int getReportAllowed() { + return mPduHeaders.getOctet(PduHeaders.REPORT_ALLOWED); + } + + /** + * Set X-Mms-Report-Allowed field value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + */ + public void setReportAllowed(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.REPORT_ALLOWED); + } + + /** + * Get X-Mms-Transaction-Id field value. + * + * @return the X-Mms-Report-Allowed value + */ + public byte[] getTransactionId() { + return mPduHeaders.getTextString(PduHeaders.TRANSACTION_ID); + } + + /** + * Set X-Mms-Transaction-Id field value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setTransactionId(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.TRANSACTION_ID); + } +} diff --git a/src/android/support/v7/mms/pdu/Base64.java b/src/android/support/v7/mms/pdu/Base64.java new file mode 100644 index 0000000..9f3418f --- /dev/null +++ b/src/android/support/v7/mms/pdu/Base64.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +public class Base64 { + /** + * Used to get the number of Quadruples. + */ + static final int FOURBYTE = 4; + + /** + * Byte used to pad output. + */ + static final byte PAD = (byte) '='; + + /** + * The base length. + */ + static final int BASELENGTH = 255; + + // Create arrays to hold the base64 characters + private static byte[] base64Alphabet = new byte[BASELENGTH]; + + // Populating the character arrays + static { + for (int i = 0; i < BASELENGTH; i++) { + base64Alphabet[i] = (byte) -1; + } + for (int i = 'Z'; i >= 'A'; i--) { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + for (int i = '9'; i >= '0'; i--) { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + } + + /** + * Decodes Base64 data into octects + * + * @param base64Data Byte array containing Base64 data + * @return Array containing decoded data. + */ + public static byte[] decodeBase64(byte[] base64Data) { + // RFC 2045 requires that we discard ALL non-Base64 characters + base64Data = discardNonBase64(base64Data); + + // handle the edge case, so we don't have to worry about it later + if (base64Data.length == 0) { + return new byte[0]; + } + + int numberQuadruple = base64Data.length / FOURBYTE; + byte decodedData[] = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0, marker0 = 0, marker1 = 0; + + // Throw away anything not in base64Data + + int encodedIndex = 0; + int dataIndex = 0; + { + // this sizes the output array properly - rlw + int lastData = base64Data.length; + // ignore the '=' padding + while (base64Data[lastData - 1] == PAD) { + if (--lastData == 0) { + return new byte[0]; + } + } + decodedData = new byte[lastData - numberQuadruple]; + } + + for (int i = 0; i < numberQuadruple; i++) { + dataIndex = i * 4; + marker0 = base64Data[dataIndex + 2]; + marker1 = base64Data[dataIndex + 3]; + + b1 = base64Alphabet[base64Data[dataIndex]]; + b2 = base64Alphabet[base64Data[dataIndex + 1]]; + + if (marker0 != PAD && marker1 != PAD) { + //No PAD e.g 3cQl + b3 = base64Alphabet[marker0]; + b4 = base64Alphabet[marker1]; + + decodedData[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex + 1] = + (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex + 2] = (byte) (b3 << 6 | b4); + } else if (marker0 == PAD) { + //Two PAD e.g. 3c[Pad][Pad] + decodedData[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + } else if (marker1 == PAD) { + //One PAD e.g. 3cQ[Pad] + b3 = base64Alphabet[marker0]; + + decodedData[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex + 1] = + (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + } + encodedIndex += 3; + } + return decodedData; + } + + /** + * Check octect wheter it is a base64 encoding. + * + * @param octect to be checked byte + * @return ture if it is base64 encoding, false otherwise. + */ + private static boolean isBase64(byte octect) { + if (octect == PAD) { + return true; + } else if (base64Alphabet[octect] == -1) { + return false; + } else { + return true; + } + } + + /** + * Discards any characters outside of the base64 alphabet, per + * the requirements on page 25 of RFC 2045 - "Any characters + * outside of the base64 alphabet are to be ignored in base64 + * encoded data." + * + * @param data The base-64 encoded data to groom + * @return The data, less non-base64 characters (see RFC 2045). + */ + static byte[] discardNonBase64(byte[] data) { + byte groomedData[] = new byte[data.length]; + int bytesCopied = 0; + + for (int i = 0; i < data.length; i++) { + if (isBase64(data[i])) { + groomedData[bytesCopied++] = data[i]; + } + } + + byte packedData[] = new byte[bytesCopied]; + + System.arraycopy(groomedData, 0, packedData, 0, bytesCopied); + + return packedData; + } +} diff --git a/src/android/support/v7/mms/pdu/CharacterSets.java b/src/android/support/v7/mms/pdu/CharacterSets.java new file mode 100644 index 0000000..8ce2a02 --- /dev/null +++ b/src/android/support/v7/mms/pdu/CharacterSets.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +import java.io.UnsupportedEncodingException; +import java.util.HashMap; + +public class CharacterSets { + /** + * IANA assigned MIB enum numbers. + * + * From wap-230-wsp-20010705-a.pdf + * Any-charset = <Octet 128> + * Equivalent to the special RFC2616 charset value "*" + */ + public static final int ANY_CHARSET = 0x00; + public static final int US_ASCII = 0x03; + public static final int ISO_8859_1 = 0x04; + public static final int ISO_8859_2 = 0x05; + public static final int ISO_8859_3 = 0x06; + public static final int ISO_8859_4 = 0x07; + public static final int ISO_8859_5 = 0x08; + public static final int ISO_8859_6 = 0x09; + public static final int ISO_8859_7 = 0x0A; + public static final int ISO_8859_8 = 0x0B; + public static final int ISO_8859_9 = 0x0C; + public static final int SHIFT_JIS = 0x11; + public static final int UTF_8 = 0x6A; + public static final int BIG5 = 0x07EA; + public static final int UCS2 = 0x03E8; + public static final int UTF_16 = 0x03F7; + + /** + * If the encoding of given data is unsupported, use UTF_8 to decode it. + */ + public static final int DEFAULT_CHARSET = UTF_8; + + /** + * Array of MIB enum numbers. + */ + private static final int[] MIBENUM_NUMBERS = { + ANY_CHARSET, + US_ASCII, + ISO_8859_1, + ISO_8859_2, + ISO_8859_3, + ISO_8859_4, + ISO_8859_5, + ISO_8859_6, + ISO_8859_7, + ISO_8859_8, + ISO_8859_9, + SHIFT_JIS, + UTF_8, + BIG5, + UCS2, + UTF_16, + }; + + /** + * The Well-known-charset Mime name. + */ + public static final String MIMENAME_ANY_CHARSET = "*"; + public static final String MIMENAME_US_ASCII = "us-ascii"; + public static final String MIMENAME_ISO_8859_1 = "iso-8859-1"; + public static final String MIMENAME_ISO_8859_2 = "iso-8859-2"; + public static final String MIMENAME_ISO_8859_3 = "iso-8859-3"; + public static final String MIMENAME_ISO_8859_4 = "iso-8859-4"; + public static final String MIMENAME_ISO_8859_5 = "iso-8859-5"; + public static final String MIMENAME_ISO_8859_6 = "iso-8859-6"; + public static final String MIMENAME_ISO_8859_7 = "iso-8859-7"; + public static final String MIMENAME_ISO_8859_8 = "iso-8859-8"; + public static final String MIMENAME_ISO_8859_9 = "iso-8859-9"; + public static final String MIMENAME_SHIFT_JIS = "shift_JIS"; + public static final String MIMENAME_UTF_8 = "utf-8"; + public static final String MIMENAME_BIG5 = "big5"; + public static final String MIMENAME_UCS2 = "iso-10646-ucs-2"; + public static final String MIMENAME_UTF_16 = "utf-16"; + + public static final String DEFAULT_CHARSET_NAME = MIMENAME_UTF_8; + + /** + * Array of the names of character sets. + */ + private static final String[] MIME_NAMES = { + MIMENAME_ANY_CHARSET, + MIMENAME_US_ASCII, + MIMENAME_ISO_8859_1, + MIMENAME_ISO_8859_2, + MIMENAME_ISO_8859_3, + MIMENAME_ISO_8859_4, + MIMENAME_ISO_8859_5, + MIMENAME_ISO_8859_6, + MIMENAME_ISO_8859_7, + MIMENAME_ISO_8859_8, + MIMENAME_ISO_8859_9, + MIMENAME_SHIFT_JIS, + MIMENAME_UTF_8, + MIMENAME_BIG5, + MIMENAME_UCS2, + MIMENAME_UTF_16, + }; + + private static final HashMap<Integer, String> MIBENUM_TO_NAME_MAP; + private static final HashMap<String, Integer> NAME_TO_MIBENUM_MAP; + + static { + // Create the HashMaps. + MIBENUM_TO_NAME_MAP = new HashMap<Integer, String>(); + NAME_TO_MIBENUM_MAP = new HashMap<String, Integer>(); + assert(MIBENUM_NUMBERS.length == MIME_NAMES.length); + int count = MIBENUM_NUMBERS.length - 1; + for(int i = 0; i <= count; i++) { + MIBENUM_TO_NAME_MAP.put(MIBENUM_NUMBERS[i], MIME_NAMES[i]); + NAME_TO_MIBENUM_MAP.put(MIME_NAMES[i], MIBENUM_NUMBERS[i]); + } + } + + private CharacterSets() {} // Non-instantiatable + + /** + * Map an MIBEnum number to the name of the charset which this number + * is assigned to by IANA. + * + * @param mibEnumValue An IANA assigned MIBEnum number. + * @return The name string of the charset. + * @throws UnsupportedEncodingException + */ + public static String getMimeName(int mibEnumValue) + throws UnsupportedEncodingException { + String name = MIBENUM_TO_NAME_MAP.get(mibEnumValue); + if (name == null) { + throw new UnsupportedEncodingException(); + } + return name; + } + + /** + * Map a well-known charset name to its assigned MIBEnum number. + * + * @param mimeName The charset name. + * @return The MIBEnum number assigned by IANA for this charset. + * @throws UnsupportedEncodingException + */ + public static int getMibEnumValue(String mimeName) + throws UnsupportedEncodingException { + if(null == mimeName) { + return -1; + } + + Integer mibEnumValue = NAME_TO_MIBENUM_MAP.get(mimeName); + if (mibEnumValue == null) { + throw new UnsupportedEncodingException(); + } + return mibEnumValue; + } +} diff --git a/src/android/support/v7/mms/pdu/ContentType.java b/src/android/support/v7/mms/pdu/ContentType.java new file mode 100644 index 0000000..7ba961d --- /dev/null +++ b/src/android/support/v7/mms/pdu/ContentType.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2007-2008 Esmertec AG. + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +import java.util.ArrayList; + +public class ContentType { + public static final String MMS_MESSAGE = "application/vnd.wap.mms-message"; + // The phony content type for generic PDUs (e.g. ReadOrig.ind, + // Notification.ind, Delivery.ind). + public static final String MMS_GENERIC = "application/vnd.wap.mms-generic"; + public static final String MULTIPART_MIXED = "application/vnd.wap.multipart.mixed"; + public static final String MULTIPART_RELATED = "application/vnd.wap.multipart.related"; + public static final String MULTIPART_ALTERNATIVE = "application/vnd.wap.multipart.alternative"; + + public static final String TEXT_PLAIN = "text/plain"; + public static final String TEXT_HTML = "text/html"; + public static final String TEXT_VCALENDAR = "text/x-vCalendar"; + public static final String TEXT_VCARD = "text/x-vCard"; + + public static final String IMAGE_UNSPECIFIED = "image/*"; + public static final String IMAGE_JPEG = "image/jpeg"; + public static final String IMAGE_JPG = "image/jpg"; + public static final String IMAGE_GIF = "image/gif"; + public static final String IMAGE_WBMP = "image/vnd.wap.wbmp"; + public static final String IMAGE_PNG = "image/png"; + public static final String IMAGE_X_MS_BMP = "image/x-ms-bmp"; + + public static final String AUDIO_UNSPECIFIED = "audio/*"; + public static final String AUDIO_AAC = "audio/aac"; + public static final String AUDIO_AMR = "audio/amr"; + public static final String AUDIO_IMELODY = "audio/imelody"; + public static final String AUDIO_MID = "audio/mid"; + public static final String AUDIO_MIDI = "audio/midi"; + public static final String AUDIO_MP3 = "audio/mp3"; + public static final String AUDIO_MPEG3 = "audio/mpeg3"; + public static final String AUDIO_MPEG = "audio/mpeg"; + public static final String AUDIO_MPG = "audio/mpg"; + public static final String AUDIO_MP4 = "audio/mp4"; + public static final String AUDIO_X_MID = "audio/x-mid"; + public static final String AUDIO_X_MIDI = "audio/x-midi"; + public static final String AUDIO_X_MP3 = "audio/x-mp3"; + public static final String AUDIO_X_MPEG3 = "audio/x-mpeg3"; + public static final String AUDIO_X_MPEG = "audio/x-mpeg"; + public static final String AUDIO_X_MPG = "audio/x-mpg"; + public static final String AUDIO_3GPP = "audio/3gpp"; + public static final String AUDIO_X_WAV = "audio/x-wav"; + public static final String AUDIO_OGG = "application/ogg"; + + public static final String VIDEO_UNSPECIFIED = "video/*"; + public static final String VIDEO_3GPP = "video/3gpp"; + public static final String VIDEO_3G2 = "video/3gpp2"; + public static final String VIDEO_H263 = "video/h263"; + public static final String VIDEO_MP4 = "video/mp4"; + + public static final String APP_SMIL = "application/smil"; + public static final String APP_WAP_XHTML = "application/vnd.wap.xhtml+xml"; + public static final String APP_XHTML = "application/xhtml+xml"; + + public static final String APP_DRM_CONTENT = "application/vnd.oma.drm.content"; + public static final String APP_DRM_MESSAGE = "application/vnd.oma.drm.message"; + + private static final ArrayList<String> sSupportedContentTypes = new ArrayList<String>(); + private static final ArrayList<String> sSupportedImageTypes = new ArrayList<String>(); + private static final ArrayList<String> sSupportedAudioTypes = new ArrayList<String>(); + private static final ArrayList<String> sSupportedVideoTypes = new ArrayList<String>(); + + static { + sSupportedContentTypes.add(TEXT_PLAIN); + sSupportedContentTypes.add(TEXT_HTML); + sSupportedContentTypes.add(TEXT_VCALENDAR); + sSupportedContentTypes.add(TEXT_VCARD); + + sSupportedContentTypes.add(IMAGE_JPEG); + sSupportedContentTypes.add(IMAGE_GIF); + sSupportedContentTypes.add(IMAGE_WBMP); + sSupportedContentTypes.add(IMAGE_PNG); + sSupportedContentTypes.add(IMAGE_JPG); + sSupportedContentTypes.add(IMAGE_X_MS_BMP); + //supportedContentTypes.add(IMAGE_SVG); not yet supported. + + sSupportedContentTypes.add(AUDIO_AAC); + sSupportedContentTypes.add(AUDIO_AMR); + sSupportedContentTypes.add(AUDIO_IMELODY); + sSupportedContentTypes.add(AUDIO_MID); + sSupportedContentTypes.add(AUDIO_MIDI); + sSupportedContentTypes.add(AUDIO_MP3); + sSupportedContentTypes.add(AUDIO_MP4); + sSupportedContentTypes.add(AUDIO_MPEG3); + sSupportedContentTypes.add(AUDIO_MPEG); + sSupportedContentTypes.add(AUDIO_MPG); + sSupportedContentTypes.add(AUDIO_X_MID); + sSupportedContentTypes.add(AUDIO_X_MIDI); + sSupportedContentTypes.add(AUDIO_X_MP3); + sSupportedContentTypes.add(AUDIO_X_MPEG3); + sSupportedContentTypes.add(AUDIO_X_MPEG); + sSupportedContentTypes.add(AUDIO_X_MPG); + sSupportedContentTypes.add(AUDIO_X_WAV); + sSupportedContentTypes.add(AUDIO_3GPP); + sSupportedContentTypes.add(AUDIO_OGG); + + sSupportedContentTypes.add(VIDEO_3GPP); + sSupportedContentTypes.add(VIDEO_3G2); + sSupportedContentTypes.add(VIDEO_H263); + sSupportedContentTypes.add(VIDEO_MP4); + + sSupportedContentTypes.add(APP_SMIL); + sSupportedContentTypes.add(APP_WAP_XHTML); + sSupportedContentTypes.add(APP_XHTML); + + sSupportedContentTypes.add(APP_DRM_CONTENT); + sSupportedContentTypes.add(APP_DRM_MESSAGE); + + // add supported image types + sSupportedImageTypes.add(IMAGE_JPEG); + sSupportedImageTypes.add(IMAGE_GIF); + sSupportedImageTypes.add(IMAGE_WBMP); + sSupportedImageTypes.add(IMAGE_PNG); + sSupportedImageTypes.add(IMAGE_JPG); + sSupportedImageTypes.add(IMAGE_X_MS_BMP); + + // add supported audio types + sSupportedAudioTypes.add(AUDIO_AAC); + sSupportedAudioTypes.add(AUDIO_AMR); + sSupportedAudioTypes.add(AUDIO_IMELODY); + sSupportedAudioTypes.add(AUDIO_MID); + sSupportedAudioTypes.add(AUDIO_MIDI); + sSupportedAudioTypes.add(AUDIO_MP3); + sSupportedAudioTypes.add(AUDIO_MPEG3); + sSupportedAudioTypes.add(AUDIO_MPEG); + sSupportedAudioTypes.add(AUDIO_MPG); + sSupportedAudioTypes.add(AUDIO_MP4); + sSupportedAudioTypes.add(AUDIO_X_MID); + sSupportedAudioTypes.add(AUDIO_X_MIDI); + sSupportedAudioTypes.add(AUDIO_X_MP3); + sSupportedAudioTypes.add(AUDIO_X_MPEG3); + sSupportedAudioTypes.add(AUDIO_X_MPEG); + sSupportedAudioTypes.add(AUDIO_X_MPG); + sSupportedAudioTypes.add(AUDIO_X_WAV); + sSupportedAudioTypes.add(AUDIO_3GPP); + sSupportedAudioTypes.add(AUDIO_OGG); + + // add supported video types + sSupportedVideoTypes.add(VIDEO_3GPP); + sSupportedVideoTypes.add(VIDEO_3G2); + sSupportedVideoTypes.add(VIDEO_H263); + sSupportedVideoTypes.add(VIDEO_MP4); + } + + // This class should never be instantiated. + private ContentType() { + } + + public static boolean isSupportedType(String contentType) { + return (null != contentType) && sSupportedContentTypes.contains(contentType); + } + + public static boolean isSupportedImageType(String contentType) { + return isImageType(contentType) && isSupportedType(contentType); + } + + public static boolean isSupportedAudioType(String contentType) { + return isAudioType(contentType) && isSupportedType(contentType); + } + + public static boolean isSupportedVideoType(String contentType) { + return isVideoType(contentType) && isSupportedType(contentType); + } + + public static boolean isTextType(String contentType) { + return (null != contentType) && contentType.startsWith("text/"); + } + + public static boolean isImageType(String contentType) { + return (null != contentType) && contentType.startsWith("image/"); + } + + public static boolean isAudioType(String contentType) { + return (null != contentType) && contentType.startsWith("audio/"); + } + + public static boolean isVideoType(String contentType) { + return (null != contentType) && contentType.startsWith("video/"); + } + + public static boolean isDrmType(String contentType) { + return (null != contentType) + && (contentType.equals(APP_DRM_CONTENT) + || contentType.equals(APP_DRM_MESSAGE)); + } + + public static boolean isUnspecified(String contentType) { + return (null != contentType) && contentType.endsWith("*"); + } + + @SuppressWarnings("unchecked") + public static ArrayList<String> getImageTypes() { + return (ArrayList<String>) sSupportedImageTypes.clone(); + } + + @SuppressWarnings("unchecked") + public static ArrayList<String> getAudioTypes() { + return (ArrayList<String>) sSupportedAudioTypes.clone(); + } + + @SuppressWarnings("unchecked") + public static ArrayList<String> getVideoTypes() { + return (ArrayList<String>) sSupportedVideoTypes.clone(); + } + + @SuppressWarnings("unchecked") + public static ArrayList<String> getSupportedTypes() { + return (ArrayList<String>) sSupportedContentTypes.clone(); + } +} diff --git a/src/android/support/v7/mms/pdu/DeliveryInd.java b/src/android/support/v7/mms/pdu/DeliveryInd.java new file mode 100644 index 0000000..d7d72d9 --- /dev/null +++ b/src/android/support/v7/mms/pdu/DeliveryInd.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +/** + * M-Delivery.Ind Pdu. + */ +public class DeliveryInd extends GenericPdu { + /** + * Empty constructor. + * Since the Pdu corresponding to this class is constructed + * by the Proxy-Relay server, this class is only instantiated + * by the Pdu Parser. + * + * @throws InvalidHeaderValueException if error occurs. + */ + public DeliveryInd() throws InvalidHeaderValueException { + super(); + setMessageType(PduHeaders.MESSAGE_TYPE_DELIVERY_IND); + } + + /** + * Constructor with given headers. + * + * @param headers Headers for this PDU. + */ + DeliveryInd(PduHeaders headers) { + super(headers); + } + + /** + * Get Date value. + * + * @return the value + */ + public long getDate() { + return mPduHeaders.getLongInteger(PduHeaders.DATE); + } + + /** + * Set Date value. + * + * @param value the value + */ + public void setDate(long value) { + mPduHeaders.setLongInteger(value, PduHeaders.DATE); + } + + /** + * Get Message-ID value. + * + * @return the value + */ + public byte[] getMessageId() { + return mPduHeaders.getTextString(PduHeaders.MESSAGE_ID); + } + + /** + * Set Message-ID value. + * + * @param value the value, should not be null + * @throws NullPointerException if the value is null. + */ + public void setMessageId(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.MESSAGE_ID); + } + + /** + * Get Status value. + * + * @return the value + */ + public int getStatus() { + return mPduHeaders.getOctet(PduHeaders.STATUS); + } + + /** + * Set Status value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + */ + public void setStatus(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.STATUS); + } + + /** + * Get To value. + * + * @return the value + */ + public EncodedStringValue[] getTo() { + return mPduHeaders.getEncodedStringValues(PduHeaders.TO); + } + + /** + * set To value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setTo(EncodedStringValue[] value) { + mPduHeaders.setEncodedStringValues(value, PduHeaders.TO); + } + + /* + * Optional, not supported header fields: + * + * public byte[] getApplicId() {return null;} + * public void setApplicId(byte[] value) {} + * + * public byte[] getAuxApplicId() {return null;} + * public void getAuxApplicId(byte[] value) {} + * + * public byte[] getReplyApplicId() {return 0x00;} + * public void setReplyApplicId(byte[] value) {} + * + * public EncodedStringValue getStatusText() {return null;} + * public void setStatusText(EncodedStringValue value) {} + */ +} diff --git a/src/android/support/v7/mms/pdu/EncodedStringValue.java b/src/android/support/v7/mms/pdu/EncodedStringValue.java new file mode 100644 index 0000000..d8fcacf --- /dev/null +++ b/src/android/support/v7/mms/pdu/EncodedStringValue.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2007-2008 Esmertec AG. + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; + +/** + * Encoded-string-value = Text-string | Value-length Char-set Text-string + */ +public class EncodedStringValue implements Cloneable { + private static final String TAG = "EncodedStringValue"; + private static final boolean DEBUG = false; + private static final boolean LOCAL_LOGV = false; + + /** + * The Char-set value. + */ + private int mCharacterSet; + + /** + * The Text-string value. + */ + private byte[] mData; + + /** + * Constructor. + * + * @param charset the Char-set value + * @param data the Text-string value + * @throws NullPointerException if Text-string value is null. + */ + public EncodedStringValue(int charset, byte[] data) { + // TODO: CharSet needs to be validated against MIBEnum. + if(null == data) { + throw new NullPointerException("EncodedStringValue: Text-string is null."); + } + + mCharacterSet = charset; + mData = new byte[data.length]; + System.arraycopy(data, 0, mData, 0, data.length); + } + + /** + * Constructor. + * + * @param data the Text-string value + * @throws NullPointerException if Text-string value is null. + */ + public EncodedStringValue(byte[] data) { + this(CharacterSets.DEFAULT_CHARSET, data); + } + + public EncodedStringValue(String data) { + try { + mData = data.getBytes(CharacterSets.DEFAULT_CHARSET_NAME); + mCharacterSet = CharacterSets.DEFAULT_CHARSET; + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Default encoding must be supported.", e); + } + } + + /** + * Get Char-set value. + * + * @return the value + */ + public int getCharacterSet() { + return mCharacterSet; + } + + /** + * Set Char-set value. + * + * @param charset the Char-set value + */ + public void setCharacterSet(int charset) { + // TODO: CharSet needs to be validated against MIBEnum. + mCharacterSet = charset; + } + + /** + * Get Text-string value. + * + * @return the value + */ + public byte[] getTextString() { + byte[] byteArray = new byte[mData.length]; + + System.arraycopy(mData, 0, byteArray, 0, mData.length); + return byteArray; + } + + /** + * Set Text-string value. + * + * @param textString the Text-string value + * @throws NullPointerException if Text-string value is null. + */ + public void setTextString(byte[] textString) { + if(null == textString) { + throw new NullPointerException("EncodedStringValue: Text-string is null."); + } + + mData = new byte[textString.length]; + System.arraycopy(textString, 0, mData, 0, textString.length); + } + + /** + * Convert this object to a {@link java.lang.String}. If the encoding of + * the EncodedStringValue is null or unsupported, it will be + * treated as iso-8859-1 encoding. + * + * @return The decoded String. + */ + public String getString() { + if (CharacterSets.ANY_CHARSET == mCharacterSet) { + return new String(mData); // system default encoding. + } else { + try { + String name = CharacterSets.getMimeName(mCharacterSet); + return new String(mData, name); + } catch (UnsupportedEncodingException e) { + if (LOCAL_LOGV) { + Log.v(TAG, e.getMessage(), e); + } + try { + return new String(mData, CharacterSets.MIMENAME_ISO_8859_1); + } catch (UnsupportedEncodingException _) { + return new String(mData); // system default encoding. + } + } + } + } + + /** + * Append to Text-string. + * + * @param textString the textString to append + * @throws NullPointerException if the text String is null + * or an IOException occured. + */ + public void appendTextString(byte[] textString) { + if(null == textString) { + throw new NullPointerException("Text-string is null."); + } + + if(null == mData) { + mData = new byte[textString.length]; + System.arraycopy(textString, 0, mData, 0, textString.length); + } else { + ByteArrayOutputStream newTextString = new ByteArrayOutputStream(); + try { + newTextString.write(mData); + newTextString.write(textString); + } catch (IOException e) { + e.printStackTrace(); + throw new NullPointerException( + "appendTextString: failed when write a new Text-string"); + } + + mData = newTextString.toByteArray(); + } + } + + /* + * (non-Javadoc) + * @see java.lang.Object#clone() + */ + @Override + public Object clone() throws CloneNotSupportedException { + super.clone(); + int len = mData.length; + byte[] dstBytes = new byte[len]; + System.arraycopy(mData, 0, dstBytes, 0, len); + + try { + return new EncodedStringValue(mCharacterSet, dstBytes); + } catch (Exception e) { + Log.e(TAG, "failed to clone an EncodedStringValue: " + this); + e.printStackTrace(); + throw new CloneNotSupportedException(e.getMessage()); + } + } + + /** + * Split this encoded string around matches of the given pattern. + * + * @param pattern the delimiting pattern + * @return the array of encoded strings computed by splitting this encoded + * string around matches of the given pattern + */ + public EncodedStringValue[] split(String pattern) { + String[] temp = getString().split(pattern); + EncodedStringValue[] ret = new EncodedStringValue[temp.length]; + for (int i = 0; i < ret.length; ++i) { + try { + ret[i] = new EncodedStringValue(mCharacterSet, + temp[i].getBytes()); + } catch (NullPointerException _) { + // Can't arrive here + return null; + } + } + return ret; + } + + /** + * Extract an EncodedStringValue[] from a given String. + */ + public static EncodedStringValue[] extract(String src) { + String[] values = src.split(";"); + + ArrayList<EncodedStringValue> list = new ArrayList<EncodedStringValue>(); + for (int i = 0; i < values.length; i++) { + if (values[i].length() > 0) { + list.add(new EncodedStringValue(values[i])); + } + } + + int len = list.size(); + if (len > 0) { + return list.toArray(new EncodedStringValue[len]); + } else { + return null; + } + } + + /** + * Concatenate an EncodedStringValue[] into a single String. + */ + public static String concat(EncodedStringValue[] addr) { + StringBuilder sb = new StringBuilder(); + int maxIndex = addr.length - 1; + for (int i = 0; i <= maxIndex; i++) { + sb.append(addr[i].getString()); + if (i < maxIndex) { + sb.append(";"); + } + } + + return sb.toString(); + } + + public static EncodedStringValue copy(EncodedStringValue value) { + if (value == null) { + return null; + } + + return new EncodedStringValue(value.mCharacterSet, value.mData); + } + + public static EncodedStringValue[] encodeStrings(String[] array) { + int count = array.length; + if (count > 0) { + EncodedStringValue[] encodedArray = new EncodedStringValue[count]; + for (int i = 0; i < count; i++) { + encodedArray[i] = new EncodedStringValue(array[i]); + } + return encodedArray; + } + return null; + } +} diff --git a/src/android/support/v7/mms/pdu/GenericPdu.java b/src/android/support/v7/mms/pdu/GenericPdu.java new file mode 100644 index 0000000..2f0d167 --- /dev/null +++ b/src/android/support/v7/mms/pdu/GenericPdu.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +public class GenericPdu { + /** + * The headers of pdu. + */ + PduHeaders mPduHeaders = null; + + /** + * Constructor. + */ + public GenericPdu() { + mPduHeaders = new PduHeaders(); + } + + /** + * Constructor. + * + * @param headers Headers for this PDU. + */ + GenericPdu(PduHeaders headers) { + mPduHeaders = headers; + } + + /** + * Get the headers of this PDU. + * + * @return A PduHeaders of this PDU. + */ + PduHeaders getPduHeaders() { + return mPduHeaders; + } + + /** + * Get X-Mms-Message-Type field value. + * + * @return the X-Mms-Report-Allowed value + */ + public int getMessageType() { + return mPduHeaders.getOctet(PduHeaders.MESSAGE_TYPE); + } + + /** + * Set X-Mms-Message-Type field value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + * RuntimeException if field's value is not Octet. + */ + public void setMessageType(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.MESSAGE_TYPE); + } + + /** + * Get X-Mms-MMS-Version field value. + * + * @return the X-Mms-MMS-Version value + */ + public int getMmsVersion() { + return mPduHeaders.getOctet(PduHeaders.MMS_VERSION); + } + + /** + * Set X-Mms-MMS-Version field value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + * RuntimeException if field's value is not Octet. + */ + public void setMmsVersion(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.MMS_VERSION); + } + + /** + * Get From value. + * From-value = Value-length + * (Address-present-token Encoded-string-value | Insert-address-token) + * + * @return the value + */ + public EncodedStringValue getFrom() { + return mPduHeaders.getEncodedStringValue(PduHeaders.FROM); + } + + /** + * Set From value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setFrom(EncodedStringValue value) { + mPduHeaders.setEncodedStringValue(value, PduHeaders.FROM); + } +} diff --git a/src/android/support/v7/mms/pdu/InvalidHeaderValueException.java b/src/android/support/v7/mms/pdu/InvalidHeaderValueException.java new file mode 100644 index 0000000..4b1de02 --- /dev/null +++ b/src/android/support/v7/mms/pdu/InvalidHeaderValueException.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +/** + * Thrown when an invalid header value was set. + */ +public class InvalidHeaderValueException extends MmsException { + private static final long serialVersionUID = -2053384496042052262L; + + /** + * Constructs an InvalidHeaderValueException with no detailed message. + */ + public InvalidHeaderValueException() { + super(); + } + + /** + * Constructs an InvalidHeaderValueException with the specified detailed message. + * + * @param message the detailed message. + */ + public InvalidHeaderValueException(String message) { + super(message); + } +} diff --git a/src/android/support/v7/mms/pdu/MmsException.java b/src/android/support/v7/mms/pdu/MmsException.java new file mode 100644 index 0000000..1fd1a02 --- /dev/null +++ b/src/android/support/v7/mms/pdu/MmsException.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +/** + * A generic exception that is thrown by the Mms client. + */ +public class MmsException extends Exception { + private static final long serialVersionUID = -7323249827281485390L; + + /** + * Creates a new MmsException. + */ + public MmsException() { + super(); + } + + /** + * Creates a new MmsException with the specified detail message. + * + * @param message the detail message. + */ + public MmsException(String message) { + super(message); + } + + /** + * Creates a new MmsException with the specified cause. + * + * @param cause the cause. + */ + public MmsException(Throwable cause) { + super(cause); + } + + /** + * Creates a new MmsException with the specified detail message and cause. + * + * @param message the detail message. + * @param cause the cause. + */ + public MmsException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/android/support/v7/mms/pdu/MultimediaMessagePdu.java b/src/android/support/v7/mms/pdu/MultimediaMessagePdu.java new file mode 100644 index 0000000..12f3b6d --- /dev/null +++ b/src/android/support/v7/mms/pdu/MultimediaMessagePdu.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +/** + * Multimedia message PDU. + */ +public class MultimediaMessagePdu extends GenericPdu{ + /** + * The body. + */ + private PduBody mMessageBody; + + /** + * Constructor. + */ + public MultimediaMessagePdu() { + super(); + } + + /** + * Constructor. + * + * @param header the header of this PDU + * @param body the body of this PDU + */ + public MultimediaMessagePdu(PduHeaders header, PduBody body) { + super(header); + mMessageBody = body; + } + + /** + * Constructor with given headers. + * + * @param headers Headers for this PDU. + */ + MultimediaMessagePdu(PduHeaders headers) { + super(headers); + } + + /** + * Get body of the PDU. + * + * @return the body + */ + public PduBody getBody() { + return mMessageBody; + } + + /** + * Set body of the PDU. + * + * @param body the body + */ + public void setBody(PduBody body) { + mMessageBody = body; + } + + /** + * Get subject. + * + * @return the value + */ + public EncodedStringValue getSubject() { + return mPduHeaders.getEncodedStringValue(PduHeaders.SUBJECT); + } + + /** + * Set subject. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setSubject(EncodedStringValue value) { + mPduHeaders.setEncodedStringValue(value, PduHeaders.SUBJECT); + } + + /** + * Get To value. + * + * @return the value + */ + public EncodedStringValue[] getTo() { + return mPduHeaders.getEncodedStringValues(PduHeaders.TO); + } + + /** + * Add a "To" value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void addTo(EncodedStringValue value) { + mPduHeaders.appendEncodedStringValue(value, PduHeaders.TO); + } + + /** + * Get X-Mms-Priority value. + * + * @return the value + */ + public int getPriority() { + return mPduHeaders.getOctet(PduHeaders.PRIORITY); + } + + /** + * Set X-Mms-Priority value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + */ + public void setPriority(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.PRIORITY); + } + + /** + * Get Date value. + * + * @return the value + */ + public long getDate() { + return mPduHeaders.getLongInteger(PduHeaders.DATE); + } + + /** + * Set Date value in seconds. + * + * @param value the value + */ + public void setDate(long value) { + mPduHeaders.setLongInteger(value, PduHeaders.DATE); + } +} diff --git a/src/android/support/v7/mms/pdu/NotificationInd.java b/src/android/support/v7/mms/pdu/NotificationInd.java new file mode 100644 index 0000000..16aa23d --- /dev/null +++ b/src/android/support/v7/mms/pdu/NotificationInd.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +/** + * M-Notification.ind PDU. + */ +public class NotificationInd extends GenericPdu { + /** + * Empty constructor. + * Since the Pdu corresponding to this class is constructed + * by the Proxy-Relay server, this class is only instantiated + * by the Pdu Parser. + * + * @throws InvalidHeaderValueException if error occurs. + * RuntimeException if an undeclared error occurs. + */ + public NotificationInd() throws InvalidHeaderValueException { + super(); + setMessageType(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND); + } + + /** + * Constructor with given headers. + * + * @param headers Headers for this PDU. + */ + NotificationInd(PduHeaders headers) { + super(headers); + } + + /** + * Get X-Mms-Content-Class Value. + * + * @return the value + */ + public int getContentClass() { + return mPduHeaders.getOctet(PduHeaders.CONTENT_CLASS); + } + + /** + * Set X-Mms-Content-Class Value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + * RuntimeException if an undeclared error occurs. + */ + public void setContentClass(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.CONTENT_CLASS); + } + + /** + * Get X-Mms-Content-Location value. + * When used in a PDU other than M-Mbox-Delete.conf and M-Delete.conf: + * Content-location-value = Uri-value + * + * @return the value + */ + public byte[] getContentLocation() { + return mPduHeaders.getTextString(PduHeaders.CONTENT_LOCATION); + } + + /** + * Set X-Mms-Content-Location value. + * + * @param value the value + * @throws NullPointerException if the value is null. + * RuntimeException if an undeclared error occurs. + */ + public void setContentLocation(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.CONTENT_LOCATION); + } + + /** + * Get X-Mms-Expiry value. + * + * Expiry-value = Value-length + * (Absolute-token Date-value | Relative-token Delta-seconds-value) + * + * @return the value + */ + public long getExpiry() { + return mPduHeaders.getLongInteger(PduHeaders.EXPIRY); + } + + /** + * Set X-Mms-Expiry value. + * + * @param value the value + * @throws RuntimeException if an undeclared error occurs. + */ + public void setExpiry(long value) { + mPduHeaders.setLongInteger(value, PduHeaders.EXPIRY); + } + + /** + * Get From value. + * From-value = Value-length + * (Address-present-token Encoded-string-value | Insert-address-token) + * + * @return the value + */ + public EncodedStringValue getFrom() { + return mPduHeaders.getEncodedStringValue(PduHeaders.FROM); + } + + /** + * Set From value. + * + * @param value the value + * @throws NullPointerException if the value is null. + * RuntimeException if an undeclared error occurs. + */ + public void setFrom(EncodedStringValue value) { + mPduHeaders.setEncodedStringValue(value, PduHeaders.FROM); + } + + /** + * Get X-Mms-Message-Class value. + * Message-class-value = Class-identifier | Token-text + * Class-identifier = Personal | Advertisement | Informational | Auto + * + * @return the value + */ + public byte[] getMessageClass() { + return mPduHeaders.getTextString(PduHeaders.MESSAGE_CLASS); + } + + /** + * Set X-Mms-Message-Class value. + * + * @param value the value + * @throws NullPointerException if the value is null. + * RuntimeException if an undeclared error occurs. + */ + public void setMessageClass(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.MESSAGE_CLASS); + } + + /** + * Get X-Mms-Message-Size value. + * Message-size-value = Long-integer + * + * @return the value + */ + public long getMessageSize() { + return mPduHeaders.getLongInteger(PduHeaders.MESSAGE_SIZE); + } + + /** + * Set X-Mms-Message-Size value. + * + * @param value the value + * @throws RuntimeException if an undeclared error occurs. + */ + public void setMessageSize(long value) { + mPduHeaders.setLongInteger(value, PduHeaders.MESSAGE_SIZE); + } + + /** + * Get subject. + * + * @return the value + */ + public EncodedStringValue getSubject() { + return mPduHeaders.getEncodedStringValue(PduHeaders.SUBJECT); + } + + /** + * Set subject. + * + * @param value the value + * @throws NullPointerException if the value is null. + * RuntimeException if an undeclared error occurs. + */ + public void setSubject(EncodedStringValue value) { + mPduHeaders.setEncodedStringValue(value, PduHeaders.SUBJECT); + } + + /** + * Get X-Mms-Transaction-Id. + * + * @return the value + */ + public byte[] getTransactionId() { + return mPduHeaders.getTextString(PduHeaders.TRANSACTION_ID); + } + + /** + * Set X-Mms-Transaction-Id. + * + * @param value the value + * @throws NullPointerException if the value is null. + * RuntimeException if an undeclared error occurs. + */ + public void setTransactionId(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.TRANSACTION_ID); + } + + /** + * Get X-Mms-Delivery-Report Value. + * + * @return the value + */ + public int getDeliveryReport() { + return mPduHeaders.getOctet(PduHeaders.DELIVERY_REPORT); + } + + /** + * Set X-Mms-Delivery-Report Value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + * RuntimeException if an undeclared error occurs. + */ + public void setDeliveryReport(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.DELIVERY_REPORT); + } + + /* + * Optional, not supported header fields: + * + * public byte[] getApplicId() {return null;} + * public void setApplicId(byte[] value) {} + * + * public byte[] getAuxApplicId() {return null;} + * public void getAuxApplicId(byte[] value) {} + * + * public byte getDrmContent() {return 0x00;} + * public void setDrmContent(byte value) {} + * + * public byte getDistributionIndicator() {return 0x00;} + * public void setDistributionIndicator(byte value) {} + * + * public ElementDescriptorValue getElementDescriptor() {return null;} + * public void getElementDescriptor(ElementDescriptorValue value) {} + * + * public byte getPriority() {return 0x00;} + * public void setPriority(byte value) {} + * + * public byte getRecommendedRetrievalMode() {return 0x00;} + * public void setRecommendedRetrievalMode(byte value) {} + * + * public byte getRecommendedRetrievalModeText() {return 0x00;} + * public void setRecommendedRetrievalModeText(byte value) {} + * + * public byte[] getReplaceId() {return 0x00;} + * public void setReplaceId(byte[] value) {} + * + * public byte[] getReplyApplicId() {return 0x00;} + * public void setReplyApplicId(byte[] value) {} + * + * public byte getReplyCharging() {return 0x00;} + * public void setReplyCharging(byte value) {} + * + * public byte getReplyChargingDeadline() {return 0x00;} + * public void setReplyChargingDeadline(byte value) {} + * + * public byte[] getReplyChargingId() {return 0x00;} + * public void setReplyChargingId(byte[] value) {} + * + * public long getReplyChargingSize() {return 0;} + * public void setReplyChargingSize(long value) {} + * + * public byte getStored() {return 0x00;} + * public void setStored(byte value) {} + */ +} diff --git a/src/android/support/v7/mms/pdu/NotifyRespInd.java b/src/android/support/v7/mms/pdu/NotifyRespInd.java new file mode 100644 index 0000000..cb8e92b --- /dev/null +++ b/src/android/support/v7/mms/pdu/NotifyRespInd.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +/** + * M-NofifyResp.ind PDU. + */ +public class NotifyRespInd extends GenericPdu { + /** + * Constructor, used when composing a M-NotifyResp.ind pdu. + * + * @param mmsVersion current version of mms + * @param transactionId the transaction-id value + * @param status the status value + * @throws InvalidHeaderValueException if parameters are invalid. + * NullPointerException if transactionId is null. + * RuntimeException if an undeclared error occurs. + */ + public NotifyRespInd(int mmsVersion, + byte[] transactionId, + int status) throws InvalidHeaderValueException { + super(); + setMessageType(PduHeaders.MESSAGE_TYPE_NOTIFYRESP_IND); + setMmsVersion(mmsVersion); + setTransactionId(transactionId); + setStatus(status); + } + + /** + * Constructor with given headers. + * + * @param headers Headers for this PDU. + */ + NotifyRespInd(PduHeaders headers) { + super(headers); + } + + /** + * Get X-Mms-Report-Allowed field value. + * + * @return the X-Mms-Report-Allowed value + */ + public int getReportAllowed() { + return mPduHeaders.getOctet(PduHeaders.REPORT_ALLOWED); + } + + /** + * Set X-Mms-Report-Allowed field value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + * RuntimeException if an undeclared error occurs. + */ + public void setReportAllowed(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.REPORT_ALLOWED); + } + + /** + * Set X-Mms-Status field value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + * RuntimeException if an undeclared error occurs. + */ + public void setStatus(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.STATUS); + } + + /** + * GetX-Mms-Status field value. + * + * @return the X-Mms-Status value + */ + public int getStatus() { + return mPduHeaders.getOctet(PduHeaders.STATUS); + } + + /** + * Get X-Mms-Transaction-Id field value. + * + * @return the X-Mms-Report-Allowed value + */ + public byte[] getTransactionId() { + return mPduHeaders.getTextString(PduHeaders.TRANSACTION_ID); + } + + /** + * Set X-Mms-Transaction-Id field value. + * + * @param value the value + * @throws NullPointerException if the value is null. + * RuntimeException if an undeclared error occurs. + */ + public void setTransactionId(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.TRANSACTION_ID); + } +} diff --git a/src/android/support/v7/mms/pdu/PduBody.java b/src/android/support/v7/mms/pdu/PduBody.java new file mode 100644 index 0000000..9d983fc --- /dev/null +++ b/src/android/support/v7/mms/pdu/PduBody.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +import java.util.HashMap; +import java.util.Map; +import java.util.Vector; + +public class PduBody { + private Vector<PduPart> mParts = null; + + private Map<String, PduPart> mPartMapByContentId = null; + private Map<String, PduPart> mPartMapByContentLocation = null; + private Map<String, PduPart> mPartMapByName = null; + private Map<String, PduPart> mPartMapByFileName = null; + + /** + * Constructor. + */ + public PduBody() { + mParts = new Vector<PduPart>(); + + mPartMapByContentId = new HashMap<String, PduPart>(); + mPartMapByContentLocation = new HashMap<String, PduPart>(); + mPartMapByName = new HashMap<String, PduPart>(); + mPartMapByFileName = new HashMap<String, PduPart>(); + } + + private void putPartToMaps(PduPart part) { + // Put part to mPartMapByContentId. + byte[] contentId = part.getContentId(); + if(null != contentId) { + mPartMapByContentId.put(new String(contentId), part); + } + + // Put part to mPartMapByContentLocation. + byte[] contentLocation = part.getContentLocation(); + if(null != contentLocation) { + String clc = new String(contentLocation); + mPartMapByContentLocation.put(clc, part); + } + + // Put part to mPartMapByName. + byte[] name = part.getName(); + if(null != name) { + String clc = new String(name); + mPartMapByName.put(clc, part); + } + + // Put part to mPartMapByFileName. + byte[] fileName = part.getFilename(); + if(null != fileName) { + String clc = new String(fileName); + mPartMapByFileName.put(clc, part); + } + } + + /** + * Appends the specified part to the end of this body. + * + * @param part part to be appended + * @return true when success, false when fail + * @throws NullPointerException when part is null + */ + public boolean addPart(PduPart part) { + if(null == part) { + throw new NullPointerException(); + } + + putPartToMaps(part); + return mParts.add(part); + } + + /** + * Inserts the specified part at the specified position. + * + * @param index index at which the specified part is to be inserted + * @param part part to be inserted + * @throws NullPointerException when part is null + */ + public void addPart(int index, PduPart part) { + if(null == part) { + throw new NullPointerException(); + } + + putPartToMaps(part); + mParts.add(index, part); + } + + /** + * Removes the part at the specified position. + * + * @param index index of the part to return + * @return part at the specified index + */ + public PduPart removePart(int index) { + return mParts.remove(index); + } + + /** + * Remove all of the parts. + */ + public void removeAll() { + mParts.clear(); + } + + /** + * Get the part at the specified position. + * + * @param index index of the part to return + * @return part at the specified index + */ + public PduPart getPart(int index) { + return mParts.get(index); + } + + /** + * Get the index of the specified part. + * + * @param part the part object + * @return index the index of the first occurrence of the part in this body + */ + public int getPartIndex(PduPart part) { + return mParts.indexOf(part); + } + + /** + * Get the number of parts. + * + * @return the number of parts + */ + public int getPartsNum() { + return mParts.size(); + } + + /** + * Get pdu part by content id. + * + * @param cid the value of content id. + * @return the pdu part. + */ + public PduPart getPartByContentId(String cid) { + return mPartMapByContentId.get(cid); + } + + /** + * Get pdu part by Content-Location. Content-Location of part is + * the same as filename and name(param of content-type). + * + * @param contentLocation the content location. + * @return the pdu part. + */ + public PduPart getPartByContentLocation(String contentLocation) { + return mPartMapByContentLocation.get(contentLocation); + } + + /** + * Get pdu part by name. + * + * @param name the value of filename. + * @return the pdu part. + */ + public PduPart getPartByName(String name) { + return mPartMapByName.get(name); + } + + /** + * Get pdu part by filename. + * + * @param filename the value of filename. + * @return the pdu part. + */ + public PduPart getPartByFileName(String filename) { + return mPartMapByFileName.get(filename); + } +} diff --git a/src/android/support/v7/mms/pdu/PduContentTypes.java b/src/android/support/v7/mms/pdu/PduContentTypes.java new file mode 100644 index 0000000..7cd0ecf --- /dev/null +++ b/src/android/support/v7/mms/pdu/PduContentTypes.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +public class PduContentTypes { + /** + * All content types. From: + * http://www.openmobilealliance.org/tech/omna/omna-wsp-content-type.htm + */ + static final String[] contentTypes = { + "*/*", /* 0x00 */ + "text/*", /* 0x01 */ + "text/html", /* 0x02 */ + "text/plain", /* 0x03 */ + "text/x-hdml", /* 0x04 */ + "text/x-ttml", /* 0x05 */ + "text/x-vCalendar", /* 0x06 */ + "text/x-vCard", /* 0x07 */ + "text/vnd.wap.wml", /* 0x08 */ + "text/vnd.wap.wmlscript", /* 0x09 */ + "text/vnd.wap.wta-event", /* 0x0A */ + "multipart/*", /* 0x0B */ + "multipart/mixed", /* 0x0C */ + "multipart/form-data", /* 0x0D */ + "multipart/byterantes", /* 0x0E */ + "multipart/alternative", /* 0x0F */ + "application/*", /* 0x10 */ + "application/java-vm", /* 0x11 */ + "application/x-www-form-urlencoded", /* 0x12 */ + "application/x-hdmlc", /* 0x13 */ + "application/vnd.wap.wmlc", /* 0x14 */ + "application/vnd.wap.wmlscriptc", /* 0x15 */ + "application/vnd.wap.wta-eventc", /* 0x16 */ + "application/vnd.wap.uaprof", /* 0x17 */ + "application/vnd.wap.wtls-ca-certificate", /* 0x18 */ + "application/vnd.wap.wtls-user-certificate", /* 0x19 */ + "application/x-x509-ca-cert", /* 0x1A */ + "application/x-x509-user-cert", /* 0x1B */ + "image/*", /* 0x1C */ + "image/gif", /* 0x1D */ + "image/jpeg", /* 0x1E */ + "image/tiff", /* 0x1F */ + "image/png", /* 0x20 */ + "image/vnd.wap.wbmp", /* 0x21 */ + "application/vnd.wap.multipart.*", /* 0x22 */ + "application/vnd.wap.multipart.mixed", /* 0x23 */ + "application/vnd.wap.multipart.form-data", /* 0x24 */ + "application/vnd.wap.multipart.byteranges", /* 0x25 */ + "application/vnd.wap.multipart.alternative", /* 0x26 */ + "application/xml", /* 0x27 */ + "text/xml", /* 0x28 */ + "application/vnd.wap.wbxml", /* 0x29 */ + "application/x-x968-cross-cert", /* 0x2A */ + "application/x-x968-ca-cert", /* 0x2B */ + "application/x-x968-user-cert", /* 0x2C */ + "text/vnd.wap.si", /* 0x2D */ + "application/vnd.wap.sic", /* 0x2E */ + "text/vnd.wap.sl", /* 0x2F */ + "application/vnd.wap.slc", /* 0x30 */ + "text/vnd.wap.co", /* 0x31 */ + "application/vnd.wap.coc", /* 0x32 */ + "application/vnd.wap.multipart.related", /* 0x33 */ + "application/vnd.wap.sia", /* 0x34 */ + "text/vnd.wap.connectivity-xml", /* 0x35 */ + "application/vnd.wap.connectivity-wbxml", /* 0x36 */ + "application/pkcs7-mime", /* 0x37 */ + "application/vnd.wap.hashed-certificate", /* 0x38 */ + "application/vnd.wap.signed-certificate", /* 0x39 */ + "application/vnd.wap.cert-response", /* 0x3A */ + "application/xhtml+xml", /* 0x3B */ + "application/wml+xml", /* 0x3C */ + "text/css", /* 0x3D */ + "application/vnd.wap.mms-message", /* 0x3E */ + "application/vnd.wap.rollover-certificate", /* 0x3F */ + "application/vnd.wap.locc+wbxml", /* 0x40 */ + "application/vnd.wap.loc+xml", /* 0x41 */ + "application/vnd.syncml.dm+wbxml", /* 0x42 */ + "application/vnd.syncml.dm+xml", /* 0x43 */ + "application/vnd.syncml.notification", /* 0x44 */ + "application/vnd.wap.xhtml+xml", /* 0x45 */ + "application/vnd.wv.csp.cir", /* 0x46 */ + "application/vnd.oma.dd+xml", /* 0x47 */ + "application/vnd.oma.drm.message", /* 0x48 */ + "application/vnd.oma.drm.content", /* 0x49 */ + "application/vnd.oma.drm.rights+xml", /* 0x4A */ + "application/vnd.oma.drm.rights+wbxml", /* 0x4B */ + "application/vnd.wv.csp+xml", /* 0x4C */ + "application/vnd.wv.csp+wbxml", /* 0x4D */ + "application/vnd.syncml.ds.notification", /* 0x4E */ + "audio/*", /* 0x4F */ + "video/*", /* 0x50 */ + "application/vnd.oma.dd2+xml", /* 0x51 */ + "application/mikey" /* 0x52 */ + }; +} diff --git a/src/android/support/v7/mms/pdu/PduHeaders.java b/src/android/support/v7/mms/pdu/PduHeaders.java new file mode 100644 index 0000000..0271e58 --- /dev/null +++ b/src/android/support/v7/mms/pdu/PduHeaders.java @@ -0,0 +1,719 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +import java.util.ArrayList; +import java.util.HashMap; + +public class PduHeaders { + /** + * All pdu header fields. + */ + public static final int BCC = 0x81; + public static final int CC = 0x82; + public static final int CONTENT_LOCATION = 0x83; + public static final int CONTENT_TYPE = 0x84; + public static final int DATE = 0x85; + public static final int DELIVERY_REPORT = 0x86; + public static final int DELIVERY_TIME = 0x87; + public static final int EXPIRY = 0x88; + public static final int FROM = 0x89; + public static final int MESSAGE_CLASS = 0x8A; + public static final int MESSAGE_ID = 0x8B; + public static final int MESSAGE_TYPE = 0x8C; + public static final int MMS_VERSION = 0x8D; + public static final int MESSAGE_SIZE = 0x8E; + public static final int PRIORITY = 0x8F; + + public static final int READ_REPLY = 0x90; + public static final int READ_REPORT = 0x90; + public static final int REPORT_ALLOWED = 0x91; + public static final int RESPONSE_STATUS = 0x92; + public static final int RESPONSE_TEXT = 0x93; + public static final int SENDER_VISIBILITY = 0x94; + public static final int STATUS = 0x95; + public static final int SUBJECT = 0x96; + public static final int TO = 0x97; + public static final int TRANSACTION_ID = 0x98; + public static final int RETRIEVE_STATUS = 0x99; + public static final int RETRIEVE_TEXT = 0x9A; + public static final int READ_STATUS = 0x9B; + public static final int REPLY_CHARGING = 0x9C; + public static final int REPLY_CHARGING_DEADLINE = 0x9D; + public static final int REPLY_CHARGING_ID = 0x9E; + public static final int REPLY_CHARGING_SIZE = 0x9F; + + public static final int PREVIOUSLY_SENT_BY = 0xA0; + public static final int PREVIOUSLY_SENT_DATE = 0xA1; + public static final int STORE = 0xA2; + public static final int MM_STATE = 0xA3; + public static final int MM_FLAGS = 0xA4; + public static final int STORE_STATUS = 0xA5; + public static final int STORE_STATUS_TEXT = 0xA6; + public static final int STORED = 0xA7; + public static final int ATTRIBUTES = 0xA8; + public static final int TOTALS = 0xA9; + public static final int MBOX_TOTALS = 0xAA; + public static final int QUOTAS = 0xAB; + public static final int MBOX_QUOTAS = 0xAC; + public static final int MESSAGE_COUNT = 0xAD; + public static final int CONTENT = 0xAE; + public static final int START = 0xAF; + + public static final int ADDITIONAL_HEADERS = 0xB0; + public static final int DISTRIBUTION_INDICATOR = 0xB1; + public static final int ELEMENT_DESCRIPTOR = 0xB2; + public static final int LIMIT = 0xB3; + public static final int RECOMMENDED_RETRIEVAL_MODE = 0xB4; + public static final int RECOMMENDED_RETRIEVAL_MODE_TEXT = 0xB5; + public static final int STATUS_TEXT = 0xB6; + public static final int APPLIC_ID = 0xB7; + public static final int REPLY_APPLIC_ID = 0xB8; + public static final int AUX_APPLIC_ID = 0xB9; + public static final int CONTENT_CLASS = 0xBA; + public static final int DRM_CONTENT = 0xBB; + public static final int ADAPTATION_ALLOWED = 0xBC; + public static final int REPLACE_ID = 0xBD; + public static final int CANCEL_ID = 0xBE; + public static final int CANCEL_STATUS = 0xBF; + + /** + * X-Mms-Message-Type field types. + */ + public static final int MESSAGE_TYPE_SEND_REQ = 0x80; + public static final int MESSAGE_TYPE_SEND_CONF = 0x81; + public static final int MESSAGE_TYPE_NOTIFICATION_IND = 0x82; + public static final int MESSAGE_TYPE_NOTIFYRESP_IND = 0x83; + public static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84; + public static final int MESSAGE_TYPE_ACKNOWLEDGE_IND = 0x85; + public static final int MESSAGE_TYPE_DELIVERY_IND = 0x86; + public static final int MESSAGE_TYPE_READ_REC_IND = 0x87; + public static final int MESSAGE_TYPE_READ_ORIG_IND = 0x88; + public static final int MESSAGE_TYPE_FORWARD_REQ = 0x89; + public static final int MESSAGE_TYPE_FORWARD_CONF = 0x8A; + public static final int MESSAGE_TYPE_MBOX_STORE_REQ = 0x8B; + public static final int MESSAGE_TYPE_MBOX_STORE_CONF = 0x8C; + public static final int MESSAGE_TYPE_MBOX_VIEW_REQ = 0x8D; + public static final int MESSAGE_TYPE_MBOX_VIEW_CONF = 0x8E; + public static final int MESSAGE_TYPE_MBOX_UPLOAD_REQ = 0x8F; + public static final int MESSAGE_TYPE_MBOX_UPLOAD_CONF = 0x90; + public static final int MESSAGE_TYPE_MBOX_DELETE_REQ = 0x91; + public static final int MESSAGE_TYPE_MBOX_DELETE_CONF = 0x92; + public static final int MESSAGE_TYPE_MBOX_DESCR = 0x93; + public static final int MESSAGE_TYPE_DELETE_REQ = 0x94; + public static final int MESSAGE_TYPE_DELETE_CONF = 0x95; + public static final int MESSAGE_TYPE_CANCEL_REQ = 0x96; + public static final int MESSAGE_TYPE_CANCEL_CONF = 0x97; + + /** + * X-Mms-Delivery-Report | + * X-Mms-Read-Report | + * X-Mms-Report-Allowed | + * X-Mms-Sender-Visibility | + * X-Mms-Store | + * X-Mms-Stored | + * X-Mms-Totals | + * X-Mms-Quotas | + * X-Mms-Distribution-Indicator | + * X-Mms-DRM-Content | + * X-Mms-Adaptation-Allowed | + * field types. + */ + public static final int VALUE_YES = 0x80; + public static final int VALUE_NO = 0x81; + + /** + * Delivery-Time | + * Expiry and Reply-Charging-Deadline | + * field type components. + */ + public static final int VALUE_ABSOLUTE_TOKEN = 0x80; + public static final int VALUE_RELATIVE_TOKEN = 0x81; + + /** + * X-Mms-MMS-Version field types. + */ + public static final int MMS_VERSION_1_3 = ((1 << 4) | 3); + public static final int MMS_VERSION_1_2 = ((1 << 4) | 2); + public static final int MMS_VERSION_1_1 = ((1 << 4) | 1); + public static final int MMS_VERSION_1_0 = ((1 << 4) | 0); + + // Current version is 1.2. + public static final int CURRENT_MMS_VERSION = MMS_VERSION_1_2; + + /** + * From field type components. + */ + public static final int FROM_ADDRESS_PRESENT_TOKEN = 0x80; + public static final int FROM_INSERT_ADDRESS_TOKEN = 0x81; + + public static final String FROM_ADDRESS_PRESENT_TOKEN_STR = "address-present-token"; + public static final String FROM_INSERT_ADDRESS_TOKEN_STR = "insert-address-token"; + + /** + * X-Mms-Status Field. + */ + public static final int STATUS_EXPIRED = 0x80; + public static final int STATUS_RETRIEVED = 0x81; + public static final int STATUS_REJECTED = 0x82; + public static final int STATUS_DEFERRED = 0x83; + public static final int STATUS_UNRECOGNIZED = 0x84; + public static final int STATUS_INDETERMINATE = 0x85; + public static final int STATUS_FORWARDED = 0x86; + public static final int STATUS_UNREACHABLE = 0x87; + + /** + * MM-Flags field type components. + */ + public static final int MM_FLAGS_ADD_TOKEN = 0x80; + public static final int MM_FLAGS_REMOVE_TOKEN = 0x81; + public static final int MM_FLAGS_FILTER_TOKEN = 0x82; + + /** + * X-Mms-Message-Class field types. + */ + public static final int MESSAGE_CLASS_PERSONAL = 0x80; + public static final int MESSAGE_CLASS_ADVERTISEMENT = 0x81; + public static final int MESSAGE_CLASS_INFORMATIONAL = 0x82; + public static final int MESSAGE_CLASS_AUTO = 0x83; + + public static final String MESSAGE_CLASS_PERSONAL_STR = "personal"; + public static final String MESSAGE_CLASS_ADVERTISEMENT_STR = "advertisement"; + public static final String MESSAGE_CLASS_INFORMATIONAL_STR = "informational"; + public static final String MESSAGE_CLASS_AUTO_STR = "auto"; + + /** + * X-Mms-Priority field types. + */ + public static final int PRIORITY_LOW = 0x80; + public static final int PRIORITY_NORMAL = 0x81; + public static final int PRIORITY_HIGH = 0x82; + + /** + * X-Mms-Response-Status field types. + */ + public static final int RESPONSE_STATUS_OK = 0x80; + public static final int RESPONSE_STATUS_ERROR_UNSPECIFIED = 0x81; + public static final int RESPONSE_STATUS_ERROR_SERVICE_DENIED = 0x82; + + public static final int RESPONSE_STATUS_ERROR_MESSAGE_FORMAT_CORRUPT = 0x83; + public static final int RESPONSE_STATUS_ERROR_SENDING_ADDRESS_UNRESOLVED = 0x84; + + public static final int RESPONSE_STATUS_ERROR_MESSAGE_NOT_FOUND = 0x85; + public static final int RESPONSE_STATUS_ERROR_NETWORK_PROBLEM = 0x86; + public static final int RESPONSE_STATUS_ERROR_CONTENT_NOT_ACCEPTED = 0x87; + public static final int RESPONSE_STATUS_ERROR_UNSUPPORTED_MESSAGE = 0x88; + public static final int RESPONSE_STATUS_ERROR_TRANSIENT_FAILURE = 0xC0; + + public static final int RESPONSE_STATUS_ERROR_TRANSIENT_SENDNG_ADDRESS_UNRESOLVED = 0xC1; + public static final int RESPONSE_STATUS_ERROR_TRANSIENT_MESSAGE_NOT_FOUND = 0xC2; + public static final int RESPONSE_STATUS_ERROR_TRANSIENT_NETWORK_PROBLEM = 0xC3; + public static final int RESPONSE_STATUS_ERROR_TRANSIENT_PARTIAL_SUCCESS = 0xC4; + + public static final int RESPONSE_STATUS_ERROR_PERMANENT_FAILURE = 0xE0; + public static final int RESPONSE_STATUS_ERROR_PERMANENT_SERVICE_DENIED = 0xE1; + public static final int RESPONSE_STATUS_ERROR_PERMANENT_MESSAGE_FORMAT_CORRUPT = 0xE2; + public static final int RESPONSE_STATUS_ERROR_PERMANENT_SENDING_ADDRESS_UNRESOLVED = 0xE3; + public static final int RESPONSE_STATUS_ERROR_PERMANENT_MESSAGE_NOT_FOUND = 0xE4; + public static final int RESPONSE_STATUS_ERROR_PERMANENT_CONTENT_NOT_ACCEPTED = 0xE5; + public static final int RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_LIMITATIONS_NOT_MET = 0xE6; + public static final int RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_REQUEST_NOT_ACCEPTED = 0xE6; + public static final int RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_FORWARDING_DENIED = 0xE8; + public static final int RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_NOT_SUPPORTED = 0xE9; + public static final int RESPONSE_STATUS_ERROR_PERMANENT_ADDRESS_HIDING_NOT_SUPPORTED = 0xEA; + public static final int RESPONSE_STATUS_ERROR_PERMANENT_LACK_OF_PREPAID = 0xEB; + public static final int RESPONSE_STATUS_ERROR_PERMANENT_END = 0xFF; + + /** + * X-Mms-Retrieve-Status field types. + */ + public static final int RETRIEVE_STATUS_OK = 0x80; + public static final int RETRIEVE_STATUS_ERROR_TRANSIENT_FAILURE = 0xC0; + public static final int RETRIEVE_STATUS_ERROR_TRANSIENT_MESSAGE_NOT_FOUND = 0xC1; + public static final int RETRIEVE_STATUS_ERROR_TRANSIENT_NETWORK_PROBLEM = 0xC2; + public static final int RETRIEVE_STATUS_ERROR_PERMANENT_FAILURE = 0xE0; + public static final int RETRIEVE_STATUS_ERROR_PERMANENT_SERVICE_DENIED = 0xE1; + public static final int RETRIEVE_STATUS_ERROR_PERMANENT_MESSAGE_NOT_FOUND = 0xE2; + public static final int RETRIEVE_STATUS_ERROR_PERMANENT_CONTENT_UNSUPPORTED = 0xE3; + public static final int RETRIEVE_STATUS_ERROR_END = 0xFF; + + /** + * X-Mms-Sender-Visibility field types. + */ + public static final int SENDER_VISIBILITY_HIDE = 0x80; + public static final int SENDER_VISIBILITY_SHOW = 0x81; + + /** + * X-Mms-Read-Status field types. + */ + public static final int READ_STATUS_READ = 0x80; + public static final int READ_STATUS__DELETED_WITHOUT_BEING_READ = 0x81; + + /** + * X-Mms-Cancel-Status field types. + */ + public static final int CANCEL_STATUS_REQUEST_SUCCESSFULLY_RECEIVED = 0x80; + public static final int CANCEL_STATUS_REQUEST_CORRUPTED = 0x81; + + /** + * X-Mms-Reply-Charging field types. + */ + public static final int REPLY_CHARGING_REQUESTED = 0x80; + public static final int REPLY_CHARGING_REQUESTED_TEXT_ONLY = 0x81; + public static final int REPLY_CHARGING_ACCEPTED = 0x82; + public static final int REPLY_CHARGING_ACCEPTED_TEXT_ONLY = 0x83; + + /** + * X-Mms-MM-State field types. + */ + public static final int MM_STATE_DRAFT = 0x80; + public static final int MM_STATE_SENT = 0x81; + public static final int MM_STATE_NEW = 0x82; + public static final int MM_STATE_RETRIEVED = 0x83; + public static final int MM_STATE_FORWARDED = 0x84; + + /** + * X-Mms-Recommended-Retrieval-Mode field types. + */ + public static final int RECOMMENDED_RETRIEVAL_MODE_MANUAL = 0x80; + + /** + * X-Mms-Content-Class field types. + */ + public static final int CONTENT_CLASS_TEXT = 0x80; + public static final int CONTENT_CLASS_IMAGE_BASIC = 0x81; + public static final int CONTENT_CLASS_IMAGE_RICH = 0x82; + public static final int CONTENT_CLASS_VIDEO_BASIC = 0x83; + public static final int CONTENT_CLASS_VIDEO_RICH = 0x84; + public static final int CONTENT_CLASS_MEGAPIXEL = 0x85; + public static final int CONTENT_CLASS_CONTENT_BASIC = 0x86; + public static final int CONTENT_CLASS_CONTENT_RICH = 0x87; + + /** + * X-Mms-Store-Status field types. + */ + public static final int STORE_STATUS_SUCCESS = 0x80; + public static final int STORE_STATUS_ERROR_TRANSIENT_FAILURE = 0xC0; + public static final int STORE_STATUS_ERROR_TRANSIENT_NETWORK_PROBLEM = 0xC1; + public static final int STORE_STATUS_ERROR_PERMANENT_FAILURE = 0xE0; + public static final int STORE_STATUS_ERROR_PERMANENT_SERVICE_DENIED = 0xE1; + public static final int STORE_STATUS_ERROR_PERMANENT_MESSAGE_FORMAT_CORRUPT = 0xE2; + public static final int STORE_STATUS_ERROR_PERMANENT_MESSAGE_NOT_FOUND = 0xE3; + public static final int STORE_STATUS_ERROR_PERMANENT_MMBOX_FULL = 0xE4; + public static final int STORE_STATUS_ERROR_END = 0xFF; + + /** + * The map contains the value of all headers. + */ + private HashMap<Integer, Object> mHeaderMap = null; + + /** + * Constructor of PduHeaders. + */ + public PduHeaders() { + mHeaderMap = new HashMap<Integer, Object>(); + } + + /** + * Get octet value by header field. + * + * @param field the field + * @return the octet value of the pdu header + * with specified header field. Return 0 if + * the value is not set. + */ + protected int getOctet(int field) { + Integer octet = (Integer) mHeaderMap.get(field); + if (null == octet) { + return 0; + } + + return octet; + } + + /** + * Set octet value to pdu header by header field. + * + * @param value the value + * @param field the field + * @throws InvalidHeaderValueException if the value is invalid. + */ + protected void setOctet(int value, int field) + throws InvalidHeaderValueException{ + /** + * Check whether this field can be set for specific + * header and check validity of the field. + */ + switch (field) { + case REPORT_ALLOWED: + case ADAPTATION_ALLOWED: + case DELIVERY_REPORT: + case DRM_CONTENT: + case DISTRIBUTION_INDICATOR: + case QUOTAS: + case READ_REPORT: + case STORE: + case STORED: + case TOTALS: + case SENDER_VISIBILITY: + if ((VALUE_YES != value) && (VALUE_NO != value)) { + // Invalid value. + throw new InvalidHeaderValueException("Invalid Octet value!"); + } + break; + case READ_STATUS: + if ((READ_STATUS_READ != value) && + (READ_STATUS__DELETED_WITHOUT_BEING_READ != value)) { + // Invalid value. + throw new InvalidHeaderValueException("Invalid Octet value!"); + } + break; + case CANCEL_STATUS: + if ((CANCEL_STATUS_REQUEST_SUCCESSFULLY_RECEIVED != value) && + (CANCEL_STATUS_REQUEST_CORRUPTED != value)) { + // Invalid value. + throw new InvalidHeaderValueException("Invalid Octet value!"); + } + break; + case PRIORITY: + if ((value < PRIORITY_LOW) || (value > PRIORITY_HIGH)) { + // Invalid value. + throw new InvalidHeaderValueException("Invalid Octet value!"); + } + break; + case STATUS: + if ((value < STATUS_EXPIRED) || (value > STATUS_UNREACHABLE)) { + // Invalid value. + throw new InvalidHeaderValueException("Invalid Octet value!"); + } + break; + case REPLY_CHARGING: + if ((value < REPLY_CHARGING_REQUESTED) + || (value > REPLY_CHARGING_ACCEPTED_TEXT_ONLY)) { + // Invalid value. + throw new InvalidHeaderValueException("Invalid Octet value!"); + } + break; + case MM_STATE: + if ((value < MM_STATE_DRAFT) || (value > MM_STATE_FORWARDED)) { + // Invalid value. + throw new InvalidHeaderValueException("Invalid Octet value!"); + } + break; + case RECOMMENDED_RETRIEVAL_MODE: + if (RECOMMENDED_RETRIEVAL_MODE_MANUAL != value) { + // Invalid value. + throw new InvalidHeaderValueException("Invalid Octet value!"); + } + break; + case CONTENT_CLASS: + if ((value < CONTENT_CLASS_TEXT) + || (value > CONTENT_CLASS_CONTENT_RICH)) { + // Invalid value. + throw new InvalidHeaderValueException("Invalid Octet value!"); + } + break; + case RETRIEVE_STATUS: + // According to oma-ts-mms-enc-v1_3, section 7.3.50, we modify the invalid value. + if ((value > RETRIEVE_STATUS_ERROR_TRANSIENT_NETWORK_PROBLEM) && + (value < RETRIEVE_STATUS_ERROR_PERMANENT_FAILURE)) { + value = RETRIEVE_STATUS_ERROR_TRANSIENT_FAILURE; + } else if ((value > RETRIEVE_STATUS_ERROR_PERMANENT_CONTENT_UNSUPPORTED) && + (value <= RETRIEVE_STATUS_ERROR_END)) { + value = RETRIEVE_STATUS_ERROR_PERMANENT_FAILURE; + } else if ((value < RETRIEVE_STATUS_OK) || + ((value > RETRIEVE_STATUS_OK) && + (value < RETRIEVE_STATUS_ERROR_TRANSIENT_FAILURE)) || + (value > RETRIEVE_STATUS_ERROR_END)) { + value = RETRIEVE_STATUS_ERROR_PERMANENT_FAILURE; + } + break; + case STORE_STATUS: + // According to oma-ts-mms-enc-v1_3, section 7.3.58, we modify the invalid value. + if ((value > STORE_STATUS_ERROR_TRANSIENT_NETWORK_PROBLEM) && + (value < STORE_STATUS_ERROR_PERMANENT_FAILURE)) { + value = STORE_STATUS_ERROR_TRANSIENT_FAILURE; + } else if ((value > STORE_STATUS_ERROR_PERMANENT_MMBOX_FULL) && + (value <= STORE_STATUS_ERROR_END)) { + value = STORE_STATUS_ERROR_PERMANENT_FAILURE; + } else if ((value < STORE_STATUS_SUCCESS) || + ((value > STORE_STATUS_SUCCESS) && + (value < STORE_STATUS_ERROR_TRANSIENT_FAILURE)) || + (value > STORE_STATUS_ERROR_END)) { + value = STORE_STATUS_ERROR_PERMANENT_FAILURE; + } + break; + case RESPONSE_STATUS: + // According to oma-ts-mms-enc-v1_3, section 7.3.48, we modify the invalid value. + if ((value > RESPONSE_STATUS_ERROR_TRANSIENT_PARTIAL_SUCCESS) && + (value < RESPONSE_STATUS_ERROR_PERMANENT_FAILURE)) { + value = RESPONSE_STATUS_ERROR_TRANSIENT_FAILURE; + } else if (((value > RESPONSE_STATUS_ERROR_PERMANENT_LACK_OF_PREPAID) && + (value <= RESPONSE_STATUS_ERROR_PERMANENT_END)) || + (value < RESPONSE_STATUS_OK) || + ((value > RESPONSE_STATUS_ERROR_UNSUPPORTED_MESSAGE) && + (value < RESPONSE_STATUS_ERROR_TRANSIENT_FAILURE)) || + (value > RESPONSE_STATUS_ERROR_PERMANENT_END)) { + value = RESPONSE_STATUS_ERROR_PERMANENT_FAILURE; + } + break; + case MMS_VERSION: + if ((value < MMS_VERSION_1_0)|| (value > MMS_VERSION_1_3)) { + value = CURRENT_MMS_VERSION; // Current version is the default value. + } + break; + case MESSAGE_TYPE: + if ((value < MESSAGE_TYPE_SEND_REQ) || (value > MESSAGE_TYPE_CANCEL_CONF)) { + // Invalid value. + throw new InvalidHeaderValueException("Invalid Octet value!"); + } + break; + default: + // This header value should not be Octect. + throw new RuntimeException("Invalid header field!"); + } + mHeaderMap.put(field, value); + } + + /** + * Get TextString value by header field. + * + * @param field the field + * @return the TextString value of the pdu header + * with specified header field + */ + protected byte[] getTextString(int field) { + return (byte[]) mHeaderMap.get(field); + } + + /** + * Set TextString value to pdu header by header field. + * + * @param value the value + * @param field the field + * @return the TextString value of the pdu header + * with specified header field + * @throws NullPointerException if the value is null. + */ + protected void setTextString(byte[] value, int field) { + /** + * Check whether this field can be set for specific + * header and check validity of the field. + */ + if (null == value) { + throw new NullPointerException(); + } + + switch (field) { + case TRANSACTION_ID: + case REPLY_CHARGING_ID: + case AUX_APPLIC_ID: + case APPLIC_ID: + case REPLY_APPLIC_ID: + case MESSAGE_ID: + case REPLACE_ID: + case CANCEL_ID: + case CONTENT_LOCATION: + case MESSAGE_CLASS: + case CONTENT_TYPE: + break; + default: + // This header value should not be Text-String. + throw new RuntimeException("Invalid header field!"); + } + mHeaderMap.put(field, value); + } + + /** + * Get EncodedStringValue value by header field. + * + * @param field the field + * @return the EncodedStringValue value of the pdu header + * with specified header field + */ + protected EncodedStringValue getEncodedStringValue(int field) { + return (EncodedStringValue) mHeaderMap.get(field); + } + + /** + * Get TO, CC or BCC header value. + * + * @param field the field + * @return the EncodeStringValue array of the pdu header + * with specified header field + */ + protected EncodedStringValue[] getEncodedStringValues(int field) { + ArrayList<EncodedStringValue> list = + (ArrayList<EncodedStringValue>) mHeaderMap.get(field); + if (null == list) { + return null; + } + EncodedStringValue[] values = new EncodedStringValue[list.size()]; + return list.toArray(values); + } + + /** + * Set EncodedStringValue value to pdu header by header field. + * + * @param value the value + * @param field the field + * @return the EncodedStringValue value of the pdu header + * with specified header field + * @throws NullPointerException if the value is null. + */ + protected void setEncodedStringValue(EncodedStringValue value, int field) { + /** + * Check whether this field can be set for specific + * header and check validity of the field. + */ + if (null == value) { + throw new NullPointerException(); + } + + switch (field) { + case SUBJECT: + case RECOMMENDED_RETRIEVAL_MODE_TEXT: + case RETRIEVE_TEXT: + case STATUS_TEXT: + case STORE_STATUS_TEXT: + case RESPONSE_TEXT: + case FROM: + case PREVIOUSLY_SENT_BY: + case MM_FLAGS: + break; + default: + // This header value should not be Encoded-String-Value. + throw new RuntimeException("Invalid header field!"); + } + + mHeaderMap.put(field, value); + } + + /** + * Set TO, CC or BCC header value. + * + * @param value the value + * @param field the field + * @return the EncodedStringValue value array of the pdu header + * with specified header field + * @throws NullPointerException if the value is null. + */ + protected void setEncodedStringValues(EncodedStringValue[] value, int field) { + /** + * Check whether this field can be set for specific + * header and check validity of the field. + */ + if (null == value) { + throw new NullPointerException(); + } + + switch (field) { + case BCC: + case CC: + case TO: + break; + default: + // This header value should not be Encoded-String-Value. + throw new RuntimeException("Invalid header field!"); + } + + ArrayList<EncodedStringValue> list = new ArrayList<EncodedStringValue>(); + for (int i = 0; i < value.length; i++) { + list.add(value[i]); + } + mHeaderMap.put(field, list); + } + + /** + * Append one EncodedStringValue to another. + * + * @param value the EncodedStringValue to append + * @param field the field + * @throws NullPointerException if the value is null. + */ + protected void appendEncodedStringValue(EncodedStringValue value, + int field) { + if (null == value) { + throw new NullPointerException(); + } + + switch (field) { + case BCC: + case CC: + case TO: + break; + default: + throw new RuntimeException("Invalid header field!"); + } + + ArrayList<EncodedStringValue> list = + (ArrayList<EncodedStringValue>) mHeaderMap.get(field); + if (null == list) { + list = new ArrayList<EncodedStringValue>(); + } + list.add(value); + mHeaderMap.put(field, list); + } + + /** + * Get LongInteger value by header field. + * + * @param field the field + * @return the LongInteger value of the pdu header + * with specified header field. if return -1, the + * field is not existed in pdu header. + */ + protected long getLongInteger(int field) { + Long longInteger = (Long) mHeaderMap.get(field); + if (null == longInteger) { + return -1; + } + + return longInteger.longValue(); + } + + /** + * Set LongInteger value to pdu header by header field. + * + * @param value the value + * @param field the field + */ + protected void setLongInteger(long value, int field) { + /** + * Check whether this field can be set for specific + * header and check validity of the field. + */ + switch (field) { + case DATE: + case REPLY_CHARGING_SIZE: + case MESSAGE_SIZE: + case MESSAGE_COUNT: + case START: + case LIMIT: + case DELIVERY_TIME: + case EXPIRY: + case REPLY_CHARGING_DEADLINE: + case PREVIOUSLY_SENT_DATE: + break; + default: + // This header value should not be LongInteger. + throw new RuntimeException("Invalid header field!"); + } + mHeaderMap.put(field, value); + } +} diff --git a/src/android/support/v7/mms/pdu/PduParser.java b/src/android/support/v7/mms/pdu/PduParser.java new file mode 100755 index 0000000..7d30df5 --- /dev/null +++ b/src/android/support/v7/mms/pdu/PduParser.java @@ -0,0 +1,2008 @@ +/* + * Copyright (C) 2007-2008 Esmertec AG. + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.HashMap; + +public class PduParser { + /** + * The next are WAP values defined in WSP specification. + */ + private static final int QUOTE = 127; + private static final int LENGTH_QUOTE = 31; + private static final int TEXT_MIN = 32; + private static final int TEXT_MAX = 127; + private static final int SHORT_INTEGER_MAX = 127; + private static final int SHORT_LENGTH_MAX = 30; + private static final int LONG_INTEGER_LENGTH_MAX = 8; + private static final int QUOTED_STRING_FLAG = 34; + private static final int END_STRING_FLAG = 0x00; + //The next two are used by the interface "parseWapString" to + //distinguish Text-String and Quoted-String. + private static final int TYPE_TEXT_STRING = 0; + private static final int TYPE_QUOTED_STRING = 1; + private static final int TYPE_TOKEN_STRING = 2; + + /** + * Specify the part position. + */ + private static final int THE_FIRST_PART = 0; + private static final int THE_LAST_PART = 1; + + /** + * The pdu data. + */ + private ByteArrayInputStream mPduDataStream = null; + + /** + * Store pdu headers + */ + private PduHeaders mHeaders = null; + + /** + * Store pdu parts. + */ + private PduBody mBody = null; + + /** + * Store the "type" parameter in "Content-Type" header field. + */ + private static byte[] mTypeParam = null; + + /** + * Store the "start" parameter in "Content-Type" header field. + */ + private static byte[] mStartParam = null; + + /** + * The log tag. + */ + private static final String LOG_TAG = "PduParser"; + private static final boolean DEBUG = false; + private static final boolean LOCAL_LOGV = false; + + /** + * Whether to parse content-disposition part header + */ + private final boolean mParseContentDisposition; + + /** + * Constructor. + * + * @param pduDataStream pdu data to be parsed + * @param parseContentDisposition whether to parse the Content-Disposition part header + */ + public PduParser(byte[] pduDataStream, boolean parseContentDisposition) { + mPduDataStream = new ByteArrayInputStream(pduDataStream); + mParseContentDisposition = parseContentDisposition; + } + + /** + * Parse the pdu. + * + * @return the pdu structure if parsing successfully. + * null if parsing error happened or mandatory fields are not set. + */ + public GenericPdu parse(){ + if (mPduDataStream == null) { + return null; + } + + /* parse headers */ + mHeaders = parseHeaders(mPduDataStream); + if (null == mHeaders) { + // Parse headers failed. + return null; + } + + /* get the message type */ + int messageType = mHeaders.getOctet(PduHeaders.MESSAGE_TYPE); + + /* check mandatory header fields */ + if (false == checkMandatoryHeader(mHeaders)) { + log("check mandatory headers failed!"); + return null; + } + + if ((PduHeaders.MESSAGE_TYPE_SEND_REQ == messageType) || + (PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF == messageType)) { + /* need to parse the parts */ + mBody = parseParts(mPduDataStream); + if (null == mBody) { + // Parse parts failed. + return null; + } + } + + switch (messageType) { + case PduHeaders.MESSAGE_TYPE_SEND_REQ: + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parse: MESSAGE_TYPE_SEND_REQ"); + } + SendReq sendReq = new SendReq(mHeaders, mBody); + return sendReq; + case PduHeaders.MESSAGE_TYPE_SEND_CONF: + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parse: MESSAGE_TYPE_SEND_CONF"); + } + SendConf sendConf = new SendConf(mHeaders); + return sendConf; + case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND: + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parse: MESSAGE_TYPE_NOTIFICATION_IND"); + } + NotificationInd notificationInd = + new NotificationInd(mHeaders); + return notificationInd; + case PduHeaders.MESSAGE_TYPE_NOTIFYRESP_IND: + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parse: MESSAGE_TYPE_NOTIFYRESP_IND"); + } + NotifyRespInd notifyRespInd = + new NotifyRespInd(mHeaders); + return notifyRespInd; + case PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF: + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parse: MESSAGE_TYPE_RETRIEVE_CONF"); + } + RetrieveConf retrieveConf = + new RetrieveConf(mHeaders, mBody); + + byte[] contentType = retrieveConf.getContentType(); + if (null == contentType) { + return null; + } + String ctTypeStr = new String(contentType); + if (ctTypeStr.equals(ContentType.MULTIPART_MIXED) + || ctTypeStr.equals(ContentType.MULTIPART_RELATED) + || ctTypeStr.equals(ContentType.MULTIPART_ALTERNATIVE)) { + // The MMS content type must be "application/vnd.wap.multipart.mixed" + // or "application/vnd.wap.multipart.related" + // or "application/vnd.wap.multipart.alternative" + return retrieveConf; + } else if (ctTypeStr.equals(ContentType.MULTIPART_ALTERNATIVE)) { + // "application/vnd.wap.multipart.alternative" + // should take only the first part. + PduPart firstPart = mBody.getPart(0); + mBody.removeAll(); + mBody.addPart(0, firstPart); + return retrieveConf; + } + return null; + case PduHeaders.MESSAGE_TYPE_DELIVERY_IND: + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parse: MESSAGE_TYPE_DELIVERY_IND"); + } + DeliveryInd deliveryInd = + new DeliveryInd(mHeaders); + return deliveryInd; + case PduHeaders.MESSAGE_TYPE_ACKNOWLEDGE_IND: + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parse: MESSAGE_TYPE_ACKNOWLEDGE_IND"); + } + AcknowledgeInd acknowledgeInd = + new AcknowledgeInd(mHeaders); + return acknowledgeInd; + case PduHeaders.MESSAGE_TYPE_READ_ORIG_IND: + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parse: MESSAGE_TYPE_READ_ORIG_IND"); + } + ReadOrigInd readOrigInd = + new ReadOrigInd(mHeaders); + return readOrigInd; + case PduHeaders.MESSAGE_TYPE_READ_REC_IND: + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parse: MESSAGE_TYPE_READ_REC_IND"); + } + ReadRecInd readRecInd = + new ReadRecInd(mHeaders); + return readRecInd; + default: + log("Parser doesn't support this message type in this version!"); + return null; + } + } + + /** + * Parse pdu headers. + * + * @param pduDataStream pdu data input stream + * @return headers in PduHeaders structure, null when parse fail + */ + protected PduHeaders parseHeaders(ByteArrayInputStream pduDataStream){ + if (pduDataStream == null) { + return null; + } + boolean keepParsing = true; + PduHeaders headers = new PduHeaders(); + + while (keepParsing && (pduDataStream.available() > 0)) { + pduDataStream.mark(1); + int headerField = extractByteValue(pduDataStream); + /* parse custom text header */ + if ((headerField >= TEXT_MIN) && (headerField <= TEXT_MAX)) { + pduDataStream.reset(); + byte [] bVal = parseWapString(pduDataStream, TYPE_TEXT_STRING); + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "TextHeader: " + new String(bVal)); + } + /* we should ignore it at the moment */ + continue; + } + switch (headerField) { + case PduHeaders.MESSAGE_TYPE: + { + int messageType = extractByteValue(pduDataStream); + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: messageType: " + messageType); + } + switch (messageType) { + // We don't support these kind of messages now. + case PduHeaders.MESSAGE_TYPE_FORWARD_REQ: + case PduHeaders.MESSAGE_TYPE_FORWARD_CONF: + case PduHeaders.MESSAGE_TYPE_MBOX_STORE_REQ: + case PduHeaders.MESSAGE_TYPE_MBOX_STORE_CONF: + case PduHeaders.MESSAGE_TYPE_MBOX_VIEW_REQ: + case PduHeaders.MESSAGE_TYPE_MBOX_VIEW_CONF: + case PduHeaders.MESSAGE_TYPE_MBOX_UPLOAD_REQ: + case PduHeaders.MESSAGE_TYPE_MBOX_UPLOAD_CONF: + case PduHeaders.MESSAGE_TYPE_MBOX_DELETE_REQ: + case PduHeaders.MESSAGE_TYPE_MBOX_DELETE_CONF: + case PduHeaders.MESSAGE_TYPE_MBOX_DESCR: + case PduHeaders.MESSAGE_TYPE_DELETE_REQ: + case PduHeaders.MESSAGE_TYPE_DELETE_CONF: + case PduHeaders.MESSAGE_TYPE_CANCEL_REQ: + case PduHeaders.MESSAGE_TYPE_CANCEL_CONF: + return null; + } + try { + headers.setOctet(messageType, headerField); + } catch(InvalidHeaderValueException e) { + log("Set invalid Octet value: " + messageType + + " into the header filed: " + headerField); + return null; + } catch(RuntimeException e) { + log(headerField + "is not Octet header field!"); + return null; + } + break; + } + /* Octect value */ + case PduHeaders.REPORT_ALLOWED: + case PduHeaders.ADAPTATION_ALLOWED: + case PduHeaders.DELIVERY_REPORT: + case PduHeaders.DRM_CONTENT: + case PduHeaders.DISTRIBUTION_INDICATOR: + case PduHeaders.QUOTAS: + case PduHeaders.READ_REPORT: + case PduHeaders.STORE: + case PduHeaders.STORED: + case PduHeaders.TOTALS: + case PduHeaders.SENDER_VISIBILITY: + case PduHeaders.READ_STATUS: + case PduHeaders.CANCEL_STATUS: + case PduHeaders.PRIORITY: + case PduHeaders.STATUS: + case PduHeaders.REPLY_CHARGING: + case PduHeaders.MM_STATE: + case PduHeaders.RECOMMENDED_RETRIEVAL_MODE: + case PduHeaders.CONTENT_CLASS: + case PduHeaders.RETRIEVE_STATUS: + case PduHeaders.STORE_STATUS: + /** + * The following field has a different value when + * used in the M-Mbox-Delete.conf and M-Delete.conf PDU. + * For now we ignore this fact, since we do not support these PDUs + */ + case PduHeaders.RESPONSE_STATUS: + { + int value = extractByteValue(pduDataStream); + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: byte: " + headerField + " value: " + + value); + } + + try { + headers.setOctet(value, headerField); + } catch(InvalidHeaderValueException e) { + log("Set invalid Octet value: " + value + + " into the header filed: " + headerField); + return null; + } catch(RuntimeException e) { + log(headerField + "is not Octet header field!"); + return null; + } + break; + } + + /* Long-Integer */ + case PduHeaders.DATE: + case PduHeaders.REPLY_CHARGING_SIZE: + case PduHeaders.MESSAGE_SIZE: + { + try { + long value = parseLongInteger(pduDataStream); + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: longint: " + headerField + " value: " + + value); + } + headers.setLongInteger(value, headerField); + } catch(RuntimeException e) { + log(headerField + "is not Long-Integer header field!"); + return null; + } + break; + } + + /* Integer-Value */ + case PduHeaders.MESSAGE_COUNT: + case PduHeaders.START: + case PduHeaders.LIMIT: + { + try { + long value = parseIntegerValue(pduDataStream); + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: int: " + headerField + " value: " + + value); + } + headers.setLongInteger(value, headerField); + } catch(RuntimeException e) { + log(headerField + "is not Long-Integer header field!"); + return null; + } + break; + } + + /* Text-String */ + case PduHeaders.TRANSACTION_ID: + case PduHeaders.REPLY_CHARGING_ID: + case PduHeaders.AUX_APPLIC_ID: + case PduHeaders.APPLIC_ID: + case PduHeaders.REPLY_APPLIC_ID: + /** + * The next three header fields are email addresses + * as defined in RFC2822, + * not including the characters "<" and ">" + */ + case PduHeaders.MESSAGE_ID: + case PduHeaders.REPLACE_ID: + case PduHeaders.CANCEL_ID: + /** + * The following field has a different value when + * used in the M-Mbox-Delete.conf and M-Delete.conf PDU. + * For now we ignore this fact, since we do not support these PDUs + */ + case PduHeaders.CONTENT_LOCATION: + { + byte[] value = parseWapString(pduDataStream, TYPE_TEXT_STRING); + if (null != value) { + try { + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: string: " + headerField + " value: " + + new String(value)); + } + headers.setTextString(value, headerField); + } catch(NullPointerException e) { + log("null pointer error!"); + } catch(RuntimeException e) { + log(headerField + "is not Text-String header field!"); + return null; + } + } + break; + } + + /* Encoded-string-value */ + case PduHeaders.SUBJECT: + case PduHeaders.RECOMMENDED_RETRIEVAL_MODE_TEXT: + case PduHeaders.RETRIEVE_TEXT: + case PduHeaders.STATUS_TEXT: + case PduHeaders.STORE_STATUS_TEXT: + /* the next one is not support + * M-Mbox-Delete.conf and M-Delete.conf now */ + case PduHeaders.RESPONSE_TEXT: + { + EncodedStringValue value = + parseEncodedStringValue(pduDataStream); + if (null != value) { + try { + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: encoded string: " + headerField + + " value: " + value.getString()); + } + headers.setEncodedStringValue(value, headerField); + } catch(NullPointerException e) { + log("null pointer error!"); + } catch (RuntimeException e) { + log(headerField + "is not Encoded-String-Value header field!"); + return null; + } + } + break; + } + + /* Addressing model */ + case PduHeaders.BCC: + case PduHeaders.CC: + case PduHeaders.TO: + { + EncodedStringValue value = + parseEncodedStringValue(pduDataStream); + if (null != value) { + byte[] address = value.getTextString(); + if (null != address) { + String str = new String(address); + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: (to/cc/bcc) address: " + headerField + + " value: " + str); + } + int endIndex = str.indexOf("/"); + if (endIndex > 0) { + str = str.substring(0, endIndex); + } + try { + value.setTextString(str.getBytes()); + } catch(NullPointerException e) { + log("null pointer error!"); + return null; + } + } + + try { + headers.appendEncodedStringValue(value, headerField); + } catch(NullPointerException e) { + log("null pointer error!"); + } catch(RuntimeException e) { + log(headerField + "is not Encoded-String-Value header field!"); + return null; + } + } + break; + } + + /* Value-length + * (Absolute-token Date-value | Relative-token Delta-seconds-value) */ + case PduHeaders.DELIVERY_TIME: + case PduHeaders.EXPIRY: + case PduHeaders.REPLY_CHARGING_DEADLINE: + { + /* parse Value-length */ + parseValueLength(pduDataStream); + + /* Absolute-token or Relative-token */ + int token = extractByteValue(pduDataStream); + + /* Date-value or Delta-seconds-value */ + long timeValue; + try { + timeValue = parseLongInteger(pduDataStream); + } catch(RuntimeException e) { + log(headerField + "is not Long-Integer header field!"); + return null; + } + if (PduHeaders.VALUE_RELATIVE_TOKEN == token) { + /* need to convert the Delta-seconds-value + * into Date-value */ + timeValue = System.currentTimeMillis()/1000 + timeValue; + } + + try { + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: time value: " + headerField + + " value: " + timeValue); + } + headers.setLongInteger(timeValue, headerField); + } catch(RuntimeException e) { + log(headerField + "is not Long-Integer header field!"); + return null; + } + break; + } + + case PduHeaders.FROM: { + /* From-value = + * Value-length + * (Address-present-token Encoded-string-value | Insert-address-token) + */ + EncodedStringValue from = null; + parseValueLength(pduDataStream); /* parse value-length */ + + /* Address-present-token or Insert-address-token */ + int fromToken = extractByteValue(pduDataStream); + + /* Address-present-token or Insert-address-token */ + if (PduHeaders.FROM_ADDRESS_PRESENT_TOKEN == fromToken) { + /* Encoded-string-value */ + from = parseEncodedStringValue(pduDataStream); + if (null != from) { + byte[] address = from.getTextString(); + if (null != address) { + String str = new String(address); + int endIndex = str.indexOf("/"); + if (endIndex > 0) { + str = str.substring(0, endIndex); + } + try { + from.setTextString(str.getBytes()); + } catch(NullPointerException e) { + log("null pointer error!"); + return null; + } + } + } + } else { + try { + from = new EncodedStringValue( + PduHeaders.FROM_INSERT_ADDRESS_TOKEN_STR.getBytes()); + } catch(NullPointerException e) { + log(headerField + "is not Encoded-String-Value header field!"); + return null; + } + } + + try { + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: from address: " + headerField + + " value: " + from.getString()); + } + headers.setEncodedStringValue(from, PduHeaders.FROM); + } catch(NullPointerException e) { + log("null pointer error!"); + } catch(RuntimeException e) { + log(headerField + "is not Encoded-String-Value header field!"); + return null; + } + break; + } + + case PduHeaders.MESSAGE_CLASS: { + /* Message-class-value = Class-identifier | Token-text */ + pduDataStream.mark(1); + int messageClass = extractByteValue(pduDataStream); + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: MESSAGE_CLASS: " + headerField + + " value: " + messageClass); + } + + if (messageClass >= PduHeaders.MESSAGE_CLASS_PERSONAL) { + /* Class-identifier */ + try { + if (PduHeaders.MESSAGE_CLASS_PERSONAL == messageClass) { + headers.setTextString( + PduHeaders.MESSAGE_CLASS_PERSONAL_STR.getBytes(), + PduHeaders.MESSAGE_CLASS); + } else if (PduHeaders.MESSAGE_CLASS_ADVERTISEMENT == messageClass) { + headers.setTextString( + PduHeaders.MESSAGE_CLASS_ADVERTISEMENT_STR.getBytes(), + PduHeaders.MESSAGE_CLASS); + } else if (PduHeaders.MESSAGE_CLASS_INFORMATIONAL == messageClass) { + headers.setTextString( + PduHeaders.MESSAGE_CLASS_INFORMATIONAL_STR.getBytes(), + PduHeaders.MESSAGE_CLASS); + } else if (PduHeaders.MESSAGE_CLASS_AUTO == messageClass) { + headers.setTextString( + PduHeaders.MESSAGE_CLASS_AUTO_STR.getBytes(), + PduHeaders.MESSAGE_CLASS); + } + } catch(NullPointerException e) { + log("null pointer error!"); + } catch(RuntimeException e) { + log(headerField + "is not Text-String header field!"); + return null; + } + } else { + /* Token-text */ + pduDataStream.reset(); + byte[] messageClassString = parseWapString(pduDataStream, TYPE_TEXT_STRING); + if (null != messageClassString) { + try { + headers.setTextString(messageClassString, PduHeaders.MESSAGE_CLASS); + } catch(NullPointerException e) { + log("null pointer error!"); + } catch(RuntimeException e) { + log(headerField + "is not Text-String header field!"); + return null; + } + } + } + break; + } + + case PduHeaders.MMS_VERSION: { + int version = parseShortInteger(pduDataStream); + + try { + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: MMS_VERSION: " + headerField + + " value: " + version); + } + headers.setOctet(version, PduHeaders.MMS_VERSION); + } catch(InvalidHeaderValueException e) { + log("Set invalid Octet value: " + version + + " into the header filed: " + headerField); + return null; + } catch(RuntimeException e) { + log(headerField + "is not Octet header field!"); + return null; + } + break; + } + + case PduHeaders.PREVIOUSLY_SENT_BY: { + /* Previously-sent-by-value = + * Value-length Forwarded-count-value Encoded-string-value */ + /* parse value-length */ + parseValueLength(pduDataStream); + + /* parse Forwarded-count-value */ + try { + parseIntegerValue(pduDataStream); + } catch(RuntimeException e) { + log(headerField + " is not Integer-Value"); + return null; + } + + /* parse Encoded-string-value */ + EncodedStringValue previouslySentBy = + parseEncodedStringValue(pduDataStream); + if (null != previouslySentBy) { + try { + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: PREVIOUSLY_SENT_BY: " + headerField + + " value: " + previouslySentBy.getString()); + } + headers.setEncodedStringValue(previouslySentBy, + PduHeaders.PREVIOUSLY_SENT_BY); + } catch(NullPointerException e) { + log("null pointer error!"); + } catch(RuntimeException e) { + log(headerField + "is not Encoded-String-Value header field!"); + return null; + } + } + break; + } + + case PduHeaders.PREVIOUSLY_SENT_DATE: { + /* Previously-sent-date-value = + * Value-length Forwarded-count-value Date-value */ + /* parse value-length */ + parseValueLength(pduDataStream); + + /* parse Forwarded-count-value */ + try { + parseIntegerValue(pduDataStream); + } catch(RuntimeException e) { + log(headerField + " is not Integer-Value"); + return null; + } + + /* Date-value */ + try { + long perviouslySentDate = parseLongInteger(pduDataStream); + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: PREVIOUSLY_SENT_DATE: " + headerField + + " value: " + perviouslySentDate); + } + headers.setLongInteger(perviouslySentDate, + PduHeaders.PREVIOUSLY_SENT_DATE); + } catch(RuntimeException e) { + log(headerField + "is not Long-Integer header field!"); + return null; + } + break; + } + + case PduHeaders.MM_FLAGS: { + /* MM-flags-value = + * Value-length + * ( Add-token | Remove-token | Filter-token ) + * Encoded-string-value + */ + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: MM_FLAGS: " + headerField + + " NOT REALLY SUPPORTED"); + } + + /* parse Value-length */ + parseValueLength(pduDataStream); + + /* Add-token | Remove-token | Filter-token */ + extractByteValue(pduDataStream); + + /* Encoded-string-value */ + parseEncodedStringValue(pduDataStream); + + /* not store this header filed in "headers", + * because now PduHeaders doesn't support it */ + break; + } + + /* Value-length + * (Message-total-token | Size-total-token) Integer-Value */ + case PduHeaders.MBOX_TOTALS: + case PduHeaders.MBOX_QUOTAS: + { + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: MBOX_TOTALS: " + headerField); + } + /* Value-length */ + parseValueLength(pduDataStream); + + /* Message-total-token | Size-total-token */ + extractByteValue(pduDataStream); + + /*Integer-Value*/ + try { + parseIntegerValue(pduDataStream); + } catch(RuntimeException e) { + log(headerField + " is not Integer-Value"); + return null; + } + + /* not store these headers filed in "headers", + because now PduHeaders doesn't support them */ + break; + } + + case PduHeaders.ELEMENT_DESCRIPTOR: { + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: ELEMENT_DESCRIPTOR: " + headerField); + } + parseContentType(pduDataStream, null); + + /* not store this header filed in "headers", + because now PduHeaders doesn't support it */ + break; + } + + case PduHeaders.CONTENT_TYPE: { + HashMap<Integer, Object> map = + new HashMap<Integer, Object>(); + byte[] contentType = + parseContentType(pduDataStream, map); + + if (null != contentType) { + try { + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: CONTENT_TYPE: " + headerField + + Arrays.toString(contentType)); + } + headers.setTextString(contentType, PduHeaders.CONTENT_TYPE); + } catch(NullPointerException e) { + log("null pointer error!"); + } catch(RuntimeException e) { + log(headerField + "is not Text-String header field!"); + return null; + } + } + + /* get start parameter */ + mStartParam = (byte[]) map.get(PduPart.P_START); + + /* get charset parameter */ + mTypeParam= (byte[]) map.get(PduPart.P_TYPE); + + keepParsing = false; + break; + } + + case PduHeaders.CONTENT: + case PduHeaders.ADDITIONAL_HEADERS: + case PduHeaders.ATTRIBUTES: + default: { + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "parseHeaders: Unknown header: " + headerField); + } + log("Unknown header"); + } + } + } + + return headers; + } + + /** + * Parse pdu parts. + * + * @param pduDataStream pdu data input stream + * @return parts in PduBody structure + */ + protected PduBody parseParts(ByteArrayInputStream pduDataStream) { + if (pduDataStream == null) { + return null; + } + + int count = parseUnsignedInt(pduDataStream); // get the number of parts + PduBody body = new PduBody(); + + for (int i = 0 ; i < count ; i++) { + int headerLength = parseUnsignedInt(pduDataStream); + int dataLength = parseUnsignedInt(pduDataStream); + PduPart part = new PduPart(); + int startPos = pduDataStream.available(); + if (startPos <= 0) { + // Invalid part. + return null; + } + + /* parse part's content-type */ + HashMap<Integer, Object> map = new HashMap<Integer, Object>(); + byte[] contentType = parseContentType(pduDataStream, map); + if (null != contentType) { + part.setContentType(contentType); + } else { + part.setContentType((PduContentTypes.contentTypes[0]).getBytes()); //"*/*" + } + + /* get name parameter */ + byte[] name = (byte[]) map.get(PduPart.P_NAME); + if (null != name) { + part.setName(name); + } + + /* get charset parameter */ + Integer charset = (Integer) map.get(PduPart.P_CHARSET); + if (null != charset) { + part.setCharset(charset); + } + + /* parse part's headers */ + int endPos = pduDataStream.available(); + int partHeaderLen = headerLength - (startPos - endPos); + if (partHeaderLen > 0) { + if (false == parsePartHeaders(pduDataStream, part, partHeaderLen)) { + // Parse part header faild. + return null; + } + } else if (partHeaderLen < 0) { + // Invalid length of content-type. + return null; + } + + /* FIXME: check content-id, name, filename and content location, + * if not set anyone of them, generate a default content-location + */ + if ((null == part.getContentLocation()) + && (null == part.getName()) + && (null == part.getFilename()) + && (null == part.getContentId())) { + part.setContentLocation(Long.toOctalString( + System.currentTimeMillis()).getBytes()); + } + + /* get part's data */ + if (dataLength > 0) { + byte[] partData = new byte[dataLength]; + String partContentType = new String(part.getContentType()); + pduDataStream.read(partData, 0, dataLength); + if (partContentType.equalsIgnoreCase(ContentType.MULTIPART_ALTERNATIVE)) { + // parse "multipart/vnd.wap.multipart.alternative". + PduBody childBody = parseParts(new ByteArrayInputStream(partData)); + // take the first part of children. + part = childBody.getPart(0); + } else { + // Check Content-Transfer-Encoding. + byte[] partDataEncoding = part.getContentTransferEncoding(); + if (null != partDataEncoding) { + String encoding = new String(partDataEncoding); + if (encoding.equalsIgnoreCase(PduPart.P_BASE64)) { + // Decode "base64" into "binary". + partData = Base64.decodeBase64(partData); + } else if (encoding.equalsIgnoreCase(PduPart.P_QUOTED_PRINTABLE)) { + // Decode "quoted-printable" into "binary". + partData = QuotedPrintable.decodeQuotedPrintable(partData); + } else { + // "binary" is the default encoding. + } + } + if (null == partData) { + log("Decode part data error!"); + return null; + } + part.setData(partData); + } + } + + /* add this part to body */ + if (THE_FIRST_PART == checkPartPosition(part)) { + /* this is the first part */ + body.addPart(0, part); + } else { + /* add the part to the end */ + body.addPart(part); + } + } + + return body; + } + + /** + * Log status. + * + * @param text log information + */ + private static void log(String text) { + if (LOCAL_LOGV) { + Log.v(LOG_TAG, text); + } + } + + /** + * Parse unsigned integer. + * + * @param pduDataStream pdu data input stream + * @return the integer, -1 when failed + */ + protected static int parseUnsignedInt(ByteArrayInputStream pduDataStream) { + /** + * From wap-230-wsp-20010705-a.pdf + * The maximum size of a uintvar is 32 bits. + * So it will be encoded in no more than 5 octets. + */ + assert(null != pduDataStream); + int result = 0; + int temp = pduDataStream.read(); + if (temp == -1) { + return temp; + } + + while((temp & 0x80) != 0) { + result = result << 7; + result |= temp & 0x7F; + temp = pduDataStream.read(); + if (temp == -1) { + return temp; + } + } + + result = result << 7; + result |= temp & 0x7F; + + return result; + } + + /** + * Parse value length. + * + * @param pduDataStream pdu data input stream + * @return the integer + */ + protected static int parseValueLength(ByteArrayInputStream pduDataStream) { + /** + * From wap-230-wsp-20010705-a.pdf + * Value-length = Short-length | (Length-quote Length) + * Short-length = <Any octet 0-30> + * Length-quote = <Octet 31> + * Length = Uintvar-integer + * Uintvar-integer = 1*5 OCTET + */ + assert(null != pduDataStream); + int temp = pduDataStream.read(); + assert(-1 != temp); + int first = temp & 0xFF; + + if (first <= SHORT_LENGTH_MAX) { + return first; + } else if (first == LENGTH_QUOTE) { + return parseUnsignedInt(pduDataStream); + } + + throw new RuntimeException("Value length > LENGTH_QUOTE!"); + } + + /** + * Parse encoded string value. + * + * @param pduDataStream pdu data input stream + * @return the EncodedStringValue + */ + protected static EncodedStringValue parseEncodedStringValue(ByteArrayInputStream pduDataStream){ + /** + * From OMA-TS-MMS-ENC-V1_3-20050927-C.pdf + * Encoded-string-value = Text-string | Value-length Char-set Text-string + */ + assert(null != pduDataStream); + pduDataStream.mark(1); + EncodedStringValue returnValue = null; + int charset = 0; + int temp = pduDataStream.read(); + assert(-1 != temp); + int first = temp & 0xFF; + if (first == 0) { + return new EncodedStringValue(""); + } + + pduDataStream.reset(); + if (first < TEXT_MIN) { + parseValueLength(pduDataStream); + + charset = parseShortInteger(pduDataStream); //get the "Charset" + } + + byte[] textString = parseWapString(pduDataStream, TYPE_TEXT_STRING); + + try { + if (0 != charset) { + returnValue = new EncodedStringValue(charset, textString); + } else { + returnValue = new EncodedStringValue(textString); + } + } catch(Exception e) { + return null; + } + + return returnValue; + } + + /** + * Parse Text-String or Quoted-String. + * + * @param pduDataStream pdu data input stream + * @param stringType TYPE_TEXT_STRING or TYPE_QUOTED_STRING + * @return the string without End-of-string in byte array + */ + protected static byte[] parseWapString(ByteArrayInputStream pduDataStream, + int stringType) { + assert(null != pduDataStream); + /** + * From wap-230-wsp-20010705-a.pdf + * Text-string = [Quote] *TEXT End-of-string + * If the first character in the TEXT is in the range of 128-255, + * a Quote character must precede it. + * Otherwise the Quote character must be omitted. + * The Quote is not part of the contents. + * Quote = <Octet 127> + * End-of-string = <Octet 0> + * + * Quoted-string = <Octet 34> *TEXT End-of-string + * + * Token-text = Token End-of-string + */ + + // Mark supposed beginning of Text-string + // We will have to mark again if first char is QUOTE or QUOTED_STRING_FLAG + pduDataStream.mark(1); + + // Check first char + int temp = pduDataStream.read(); + assert(-1 != temp); + if ((TYPE_QUOTED_STRING == stringType) && + (QUOTED_STRING_FLAG == temp)) { + // Mark again if QUOTED_STRING_FLAG and ignore it + pduDataStream.mark(1); + } else if ((TYPE_TEXT_STRING == stringType) && + (QUOTE == temp)) { + // Mark again if QUOTE and ignore it + pduDataStream.mark(1); + } else { + // Otherwise go back to origin + pduDataStream.reset(); + } + + // We are now definitely at the beginning of string + /** + * Return *TOKEN or *TEXT (Text-String without QUOTE, + * Quoted-String without QUOTED_STRING_FLAG and without End-of-string) + */ + return getWapString(pduDataStream, stringType); + } + + /** + * Check TOKEN data defined in RFC2616. + * @param ch checking data + * @return true when ch is TOKEN, false when ch is not TOKEN + */ + protected static boolean isTokenCharacter(int ch) { + /** + * Token = 1*<any CHAR except CTLs or separators> + * separators = "("(40) | ")"(41) | "<"(60) | ">"(62) | "@"(64) + * | ","(44) | ";"(59) | ":"(58) | "\"(92) | <">(34) + * | "/"(47) | "["(91) | "]"(93) | "?"(63) | "="(61) + * | "{"(123) | "}"(125) | SP(32) | HT(9) + * CHAR = <any US-ASCII character (octets 0 - 127)> + * CTL = <any US-ASCII control character + * (octets 0 - 31) and DEL (127)> + * SP = <US-ASCII SP, space (32)> + * HT = <US-ASCII HT, horizontal-tab (9)> + */ + if((ch < 33) || (ch > 126)) { + return false; + } + + switch(ch) { + case '"': /* '"' */ + case '(': /* '(' */ + case ')': /* ')' */ + case ',': /* ',' */ + case '/': /* '/' */ + case ':': /* ':' */ + case ';': /* ';' */ + case '<': /* '<' */ + case '=': /* '=' */ + case '>': /* '>' */ + case '?': /* '?' */ + case '@': /* '@' */ + case '[': /* '[' */ + case '\\': /* '\' */ + case ']': /* ']' */ + case '{': /* '{' */ + case '}': /* '}' */ + return false; + } + + return true; + } + + /** + * Check TEXT data defined in RFC2616. + * @param ch checking data + * @return true when ch is TEXT, false when ch is not TEXT + */ + protected static boolean isText(int ch) { + /** + * TEXT = <any OCTET except CTLs, + * but including LWS> + * CTL = <any US-ASCII control character + * (octets 0 - 31) and DEL (127)> + * LWS = [CRLF] 1*( SP | HT ) + * CRLF = CR LF + * CR = <US-ASCII CR, carriage return (13)> + * LF = <US-ASCII LF, linefeed (10)> + */ + if(((ch >= 32) && (ch <= 126)) || ((ch >= 128) && (ch <= 255))) { + return true; + } + + switch(ch) { + case '\t': /* '\t' */ + case '\n': /* '\n' */ + case '\r': /* '\r' */ + return true; + } + + return false; + } + + protected static byte[] getWapString(ByteArrayInputStream pduDataStream, + int stringType) { + assert(null != pduDataStream); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int temp = pduDataStream.read(); + assert(-1 != temp); + while((-1 != temp) && ('\0' != temp)) { + // check each of the character + if (stringType == TYPE_TOKEN_STRING) { + if (isTokenCharacter(temp)) { + out.write(temp); + } + } else { + if (isText(temp)) { + out.write(temp); + } + } + + temp = pduDataStream.read(); + assert(-1 != temp); + } + + if (out.size() > 0) { + return out.toByteArray(); + } + + return null; + } + + /** + * Extract a byte value from the input stream. + * + * @param pduDataStream pdu data input stream + * @return the byte + */ + protected static int extractByteValue(ByteArrayInputStream pduDataStream) { + assert(null != pduDataStream); + int temp = pduDataStream.read(); + assert(-1 != temp); + return temp & 0xFF; + } + + /** + * Parse Short-Integer. + * + * @param pduDataStream pdu data input stream + * @return the byte + */ + protected static int parseShortInteger(ByteArrayInputStream pduDataStream) { + /** + * From wap-230-wsp-20010705-a.pdf + * Short-integer = OCTET + * Integers in range 0-127 shall be encoded as a one + * octet value with the most significant bit set to one (1xxx xxxx) + * and with the value in the remaining least significant bits. + */ + assert(null != pduDataStream); + int temp = pduDataStream.read(); + assert(-1 != temp); + return temp & 0x7F; + } + + /** + * Parse Long-Integer. + * + * @param pduDataStream pdu data input stream + * @return long integer + */ + protected static long parseLongInteger(ByteArrayInputStream pduDataStream) { + /** + * From wap-230-wsp-20010705-a.pdf + * Long-integer = Short-length Multi-octet-integer + * The Short-length indicates the length of the Multi-octet-integer + * Multi-octet-integer = 1*30 OCTET + * The content octets shall be an unsigned integer value + * with the most significant octet encoded first (big-endian representation). + * The minimum number of octets must be used to encode the value. + * Short-length = <Any octet 0-30> + */ + assert(null != pduDataStream); + int temp = pduDataStream.read(); + assert(-1 != temp); + int count = temp & 0xFF; + + if (count > LONG_INTEGER_LENGTH_MAX) { + throw new RuntimeException("Octet count greater than 8 and I can't represent that!"); + } + + long result = 0; + + for (int i = 0 ; i < count ; i++) { + temp = pduDataStream.read(); + assert(-1 != temp); + result <<= 8; + result += (temp & 0xFF); + } + + return result; + } + + /** + * Parse Integer-Value. + * + * @param pduDataStream pdu data input stream + * @return long integer + */ + protected static long parseIntegerValue(ByteArrayInputStream pduDataStream) { + /** + * From wap-230-wsp-20010705-a.pdf + * Integer-Value = Short-integer | Long-integer + */ + assert(null != pduDataStream); + pduDataStream.mark(1); + int temp = pduDataStream.read(); + assert(-1 != temp); + pduDataStream.reset(); + if (temp > SHORT_INTEGER_MAX) { + return parseShortInteger(pduDataStream); + } else { + return parseLongInteger(pduDataStream); + } + } + + /** + * To skip length of the wap value. + * + * @param pduDataStream pdu data input stream + * @param length area size + * @return the values in this area + */ + protected static int skipWapValue(ByteArrayInputStream pduDataStream, int length) { + assert(null != pduDataStream); + byte[] area = new byte[length]; + int readLen = pduDataStream.read(area, 0, length); + if (readLen < length) { //The actually read length is lower than the length + return -1; + } else { + return readLen; + } + } + + /** + * Parse content type parameters. For now we just support + * four parameters used in mms: "type", "start", "name", "charset". + * + * @param pduDataStream pdu data input stream + * @param map to store parameters of Content-Type field + * @param length length of all the parameters + */ + protected static void parseContentTypeParams(ByteArrayInputStream pduDataStream, + HashMap<Integer, Object> map, Integer length) { + /** + * From wap-230-wsp-20010705-a.pdf + * Parameter = Typed-parameter | Untyped-parameter + * Typed-parameter = Well-known-parameter-token Typed-value + * the actual expected type of the value is implied by the well-known parameter + * Well-known-parameter-token = Integer-value + * the code values used for parameters are specified in the Assigned Numbers appendix + * Typed-value = Compact-value | Text-value + * In addition to the expected type, there may be no value. + * If the value cannot be encoded using the expected type, it shall be encoded as text. + * Compact-value = Integer-value | + * Date-value | Delta-seconds-value | Q-value | Version-value | + * Uri-value + * Untyped-parameter = Token-text Untyped-value + * the type of the value is unknown, but it shall be encoded as an integer, + * if that is possible. + * Untyped-value = Integer-value | Text-value + */ + assert(null != pduDataStream); + assert(length > 0); + + int startPos = pduDataStream.available(); + int tempPos = 0; + int lastLen = length; + while(0 < lastLen) { + int param = pduDataStream.read(); + assert(-1 != param); + lastLen--; + + switch (param) { + /** + * From rfc2387, chapter 3.1 + * The type parameter must be specified and its value is the MIME media + * type of the "root" body part. It permits a MIME user agent to + * determine the content-type without reference to the enclosed body + * part. If the value of the type parameter and the root body part's + * content-type differ then the User Agent's behavior is undefined. + * + * From wap-230-wsp-20010705-a.pdf + * type = Constrained-encoding + * Constrained-encoding = Extension-Media | Short-integer + * Extension-media = *TEXT End-of-string + */ + case PduPart.P_TYPE: + case PduPart.P_CT_MR_TYPE: + pduDataStream.mark(1); + int first = extractByteValue(pduDataStream); + pduDataStream.reset(); + if (first > TEXT_MAX) { + // Short-integer (well-known type) + int index = parseShortInteger(pduDataStream); + + if (index < PduContentTypes.contentTypes.length) { + byte[] type = (PduContentTypes.contentTypes[index]).getBytes(); + map.put(PduPart.P_TYPE, type); + } else { + //not support this type, ignore it. + } + } else { + // Text-String (extension-media) + byte[] type = parseWapString(pduDataStream, TYPE_TEXT_STRING); + if ((null != type) && (null != map)) { + map.put(PduPart.P_TYPE, type); + } + } + + tempPos = pduDataStream.available(); + lastLen = length - (startPos - tempPos); + break; + + /** + * From oma-ts-mms-conf-v1_3.pdf, chapter 10.2.3. + * Start Parameter Referring to Presentation + * + * From rfc2387, chapter 3.2 + * The start parameter, if given, is the content-ID of the compound + * object's "root". If not present the "root" is the first body part in + * the Multipart/Related entity. The "root" is the element the + * applications processes first. + * + * From wap-230-wsp-20010705-a.pdf + * start = Text-String + */ + case PduPart.P_START: + case PduPart.P_DEP_START: + byte[] start = parseWapString(pduDataStream, TYPE_TEXT_STRING); + if ((null != start) && (null != map)) { + map.put(PduPart.P_START, start); + } + + tempPos = pduDataStream.available(); + lastLen = length - (startPos - tempPos); + break; + + /** + * From oma-ts-mms-conf-v1_3.pdf + * In creation, the character set SHALL be either us-ascii + * (IANA MIBenum 3) or utf-8 (IANA MIBenum 106)[Unicode]. + * In retrieval, both us-ascii and utf-8 SHALL be supported. + * + * From wap-230-wsp-20010705-a.pdf + * charset = Well-known-charset|Text-String + * Well-known-charset = Any-charset | Integer-value + * Both are encoded using values from Character Set + * Assignments table in Assigned Numbers + * Any-charset = <Octet 128> + * Equivalent to the special RFC2616 charset value "*" + */ + case PduPart.P_CHARSET: + pduDataStream.mark(1); + int firstValue = extractByteValue(pduDataStream); + pduDataStream.reset(); + //Check first char + if (((firstValue > TEXT_MIN) && (firstValue < TEXT_MAX)) || + (END_STRING_FLAG == firstValue)) { + //Text-String (extension-charset) + byte[] charsetStr = parseWapString(pduDataStream, TYPE_TEXT_STRING); + try { + int charsetInt = CharacterSets.getMibEnumValue( + new String(charsetStr)); + map.put(PduPart.P_CHARSET, charsetInt); + } catch (UnsupportedEncodingException e) { + // Not a well-known charset, use "*". + Log.e(LOG_TAG, Arrays.toString(charsetStr), e); + map.put(PduPart.P_CHARSET, CharacterSets.ANY_CHARSET); + } + } else { + //Well-known-charset + int charset = (int) parseIntegerValue(pduDataStream); + if (map != null) { + map.put(PduPart.P_CHARSET, charset); + } + } + + tempPos = pduDataStream.available(); + lastLen = length - (startPos - tempPos); + break; + + /** + * From oma-ts-mms-conf-v1_3.pdf + * A name for multipart object SHALL be encoded using name-parameter + * for Content-Type header in WSP multipart headers. + * + * From wap-230-wsp-20010705-a.pdf + * name = Text-String + */ + case PduPart.P_DEP_NAME: + case PduPart.P_NAME: + byte[] name = parseWapString(pduDataStream, TYPE_TEXT_STRING); + if ((null != name) && (null != map)) { + map.put(PduPart.P_NAME, name); + } + + tempPos = pduDataStream.available(); + lastLen = length - (startPos - tempPos); + break; + default: + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "Not supported Content-Type parameter"); + } + if (-1 == skipWapValue(pduDataStream, lastLen)) { + Log.e(LOG_TAG, "Corrupt Content-Type"); + } else { + lastLen = 0; + } + break; + } + } + + if (0 != lastLen) { + Log.e(LOG_TAG, "Corrupt Content-Type"); + } + } + + /** + * Parse content type. + * + * @param pduDataStream pdu data input stream + * @param map to store parameters in Content-Type header field + * @return Content-Type value + */ + protected static byte[] parseContentType(ByteArrayInputStream pduDataStream, + HashMap<Integer, Object> map) { + /** + * From wap-230-wsp-20010705-a.pdf + * Content-type-value = Constrained-media | Content-general-form + * Content-general-form = Value-length Media-type + * Media-type = (Well-known-media | Extension-Media) *(Parameter) + */ + assert(null != pduDataStream); + + byte[] contentType = null; + pduDataStream.mark(1); + int temp = pduDataStream.read(); + assert(-1 != temp); + pduDataStream.reset(); + + int cur = (temp & 0xFF); + + if (cur < TEXT_MIN) { + int length = parseValueLength(pduDataStream); + int startPos = pduDataStream.available(); + pduDataStream.mark(1); + temp = pduDataStream.read(); + assert(-1 != temp); + pduDataStream.reset(); + int first = (temp & 0xFF); + + if ((first >= TEXT_MIN) && (first <= TEXT_MAX)) { + contentType = parseWapString(pduDataStream, TYPE_TEXT_STRING); + } else if (first > TEXT_MAX) { + int index = parseShortInteger(pduDataStream); + + if (index < PduContentTypes.contentTypes.length) { //well-known type + contentType = (PduContentTypes.contentTypes[index]).getBytes(); + } else { + pduDataStream.reset(); + contentType = parseWapString(pduDataStream, TYPE_TEXT_STRING); + } + } else { + Log.e(LOG_TAG, "Corrupt content-type"); + return (PduContentTypes.contentTypes[0]).getBytes(); //"*/*" + } + + int endPos = pduDataStream.available(); + int parameterLen = length - (startPos - endPos); + if (parameterLen > 0) {//have parameters + parseContentTypeParams(pduDataStream, map, parameterLen); + } + + if (parameterLen < 0) { + Log.e(LOG_TAG, "Corrupt MMS message"); + return (PduContentTypes.contentTypes[0]).getBytes(); //"*/*" + } + } else if (cur <= TEXT_MAX) { + contentType = parseWapString(pduDataStream, TYPE_TEXT_STRING); + } else { + contentType = + (PduContentTypes.contentTypes[parseShortInteger(pduDataStream)]).getBytes(); + } + + return contentType; + } + + /** + * Parse part's headers. + * + * @param pduDataStream pdu data input stream + * @param part to store the header informations of the part + * @param length length of the headers + * @return true if parse successfully, false otherwise + */ + protected boolean parsePartHeaders(ByteArrayInputStream pduDataStream, + PduPart part, int length) { + assert(null != pduDataStream); + assert(null != part); + assert(length > 0); + + /** + * From oma-ts-mms-conf-v1_3.pdf, chapter 10.2. + * 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. + * + * Within SMIL part the reference to the media object parts SHALL use + * either Content-ID or Content-Location mechanism [RFC2557] + * and the corresponding WSP part headers in media object parts + * contain the corresponding definitions. + */ + int startPos = pduDataStream.available(); + int tempPos = 0; + int lastLen = length; + while(0 < lastLen) { + int header = pduDataStream.read(); + assert(-1 != header); + lastLen--; + + if (header > TEXT_MAX) { + // Number assigned headers. + switch (header) { + case PduPart.P_CONTENT_LOCATION: + /** + * From wap-230-wsp-20010705-a.pdf, chapter 8.4.2.21 + * Content-location-value = Uri-value + */ + byte[] contentLocation = parseWapString(pduDataStream, TYPE_TEXT_STRING); + if (null != contentLocation) { + part.setContentLocation(contentLocation); + } + + tempPos = pduDataStream.available(); + lastLen = length - (startPos - tempPos); + break; + case PduPart.P_CONTENT_ID: + /** + * From wap-230-wsp-20010705-a.pdf, chapter 8.4.2.21 + * Content-ID-value = Quoted-string + */ + byte[] contentId = parseWapString(pduDataStream, TYPE_QUOTED_STRING); + if (null != contentId) { + part.setContentId(contentId); + } + + tempPos = pduDataStream.available(); + lastLen = length - (startPos - tempPos); + break; + case PduPart.P_DEP_CONTENT_DISPOSITION: + case PduPart.P_CONTENT_DISPOSITION: + /** + * From wap-230-wsp-20010705-a.pdf, chapter 8.4.2.21 + * Content-disposition-value = Value-length Disposition *(Parameter) + * Disposition = Form-data | Attachment | Inline | Token-text + * Form-data = <Octet 128> + * Attachment = <Octet 129> + * Inline = <Octet 130> + */ + + /* + * some carrier mmsc servers do not support content_disposition + * field correctly + */ + if (mParseContentDisposition) { + int len = parseValueLength(pduDataStream); + pduDataStream.mark(1); + int thisStartPos = pduDataStream.available(); + int thisEndPos = 0; + int value = pduDataStream.read(); + + if (value == PduPart.P_DISPOSITION_FROM_DATA ) { + part.setContentDisposition(PduPart.DISPOSITION_FROM_DATA); + } else if (value == PduPart.P_DISPOSITION_ATTACHMENT) { + part.setContentDisposition(PduPart.DISPOSITION_ATTACHMENT); + } else if (value == PduPart.P_DISPOSITION_INLINE) { + part.setContentDisposition(PduPart.DISPOSITION_INLINE); + } else { + pduDataStream.reset(); + /* Token-text */ + part.setContentDisposition(parseWapString(pduDataStream + , TYPE_TEXT_STRING)); + } + + /* get filename parameter and skip other parameters */ + thisEndPos = pduDataStream.available(); + if (thisStartPos - thisEndPos < len) { + value = pduDataStream.read(); + if (value == PduPart.P_FILENAME) { //filename is text-string + part.setFilename(parseWapString(pduDataStream + , TYPE_TEXT_STRING)); + } + + /* skip other parameters */ + thisEndPos = pduDataStream.available(); + if (thisStartPos - thisEndPos < len) { + int last = len - (thisStartPos - thisEndPos); + byte[] temp = new byte[last]; + pduDataStream.read(temp, 0, last); + } + } + + tempPos = pduDataStream.available(); + lastLen = length - (startPos - tempPos); + } + break; + default: + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "Not supported Part headers: " + header); + } + if (-1 == skipWapValue(pduDataStream, lastLen)) { + Log.e(LOG_TAG, "Corrupt Part headers"); + return false; + } + lastLen = 0; + break; + } + } else if ((header >= TEXT_MIN) && (header <= TEXT_MAX)) { + // Not assigned header. + byte[] tempHeader = parseWapString(pduDataStream, TYPE_TEXT_STRING); + byte[] tempValue = parseWapString(pduDataStream, TYPE_TEXT_STRING); + + // Check the header whether it is "Content-Transfer-Encoding". + if (true == + PduPart.CONTENT_TRANSFER_ENCODING.equalsIgnoreCase(new String(tempHeader))) { + part.setContentTransferEncoding(tempValue); + } + + tempPos = pduDataStream.available(); + lastLen = length - (startPos - tempPos); + } else { + if (LOCAL_LOGV) { + Log.v(LOG_TAG, "Not supported Part headers: " + header); + } + // Skip all headers of this part. + if (-1 == skipWapValue(pduDataStream, lastLen)) { + Log.e(LOG_TAG, "Corrupt Part headers"); + return false; + } + lastLen = 0; + } + } + + if (0 != lastLen) { + Log.e(LOG_TAG, "Corrupt Part headers"); + return false; + } + + return true; + } + + /** + * Check the position of a specified part. + * + * @param part the part to be checked + * @return part position, THE_FIRST_PART when it's the + * first one, THE_LAST_PART when it's the last one. + */ + private static int checkPartPosition(PduPart part) { + assert(null != part); + if ((null == mTypeParam) && + (null == mStartParam)) { + return THE_LAST_PART; + } + + /* check part's content-id */ + if (null != mStartParam) { + byte[] contentId = part.getContentId(); + if (null != contentId) { + if (true == Arrays.equals(mStartParam, contentId)) { + return THE_FIRST_PART; + } + } + // This is not the first part, so append to end (keeping the original order) + // Check b/19607294 for details of this change + return THE_LAST_PART; + } + + /* check part's content-type */ + if (null != mTypeParam) { + byte[] contentType = part.getContentType(); + if (null != contentType) { + if (true == Arrays.equals(mTypeParam, contentType)) { + return THE_FIRST_PART; + } + } + } + + return THE_LAST_PART; + } + + /** + * Check mandatory headers of a pdu. + * + * @param headers pdu headers + * @return true if the pdu has all of the mandatory headers, false otherwise. + */ + protected static boolean checkMandatoryHeader(PduHeaders headers) { + if (null == headers) { + return false; + } + + /* get message type */ + int messageType = headers.getOctet(PduHeaders.MESSAGE_TYPE); + + /* check Mms-Version field */ + int mmsVersion = headers.getOctet(PduHeaders.MMS_VERSION); + if (0 == mmsVersion) { + // Every message should have Mms-Version field. + return false; + } + + /* check mandatory header fields */ + switch (messageType) { + case PduHeaders.MESSAGE_TYPE_SEND_REQ: + // Content-Type field. + byte[] srContentType = headers.getTextString(PduHeaders.CONTENT_TYPE); + if (null == srContentType) { + return false; + } + + // From field. + EncodedStringValue srFrom = headers.getEncodedStringValue(PduHeaders.FROM); + if (null == srFrom) { + return false; + } + + // Transaction-Id field. + byte[] srTransactionId = headers.getTextString(PduHeaders.TRANSACTION_ID); + if (null == srTransactionId) { + return false; + } + + break; + case PduHeaders.MESSAGE_TYPE_SEND_CONF: + // Response-Status field. + int scResponseStatus = headers.getOctet(PduHeaders.RESPONSE_STATUS); + if (0 == scResponseStatus) { + return false; + } + + // Transaction-Id field. + byte[] scTransactionId = headers.getTextString(PduHeaders.TRANSACTION_ID); + if (null == scTransactionId) { + return false; + } + + break; + case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND: + // Content-Location field. + byte[] niContentLocation = headers.getTextString(PduHeaders.CONTENT_LOCATION); + if (null == niContentLocation) { + return false; + } + + // Expiry field. + long niExpiry = headers.getLongInteger(PduHeaders.EXPIRY); + if (-1 == niExpiry) { + return false; + } + + // Message-Class field. + byte[] niMessageClass = headers.getTextString(PduHeaders.MESSAGE_CLASS); + if (null == niMessageClass) { + return false; + } + + // Message-Size field. + long niMessageSize = headers.getLongInteger(PduHeaders.MESSAGE_SIZE); + if (-1 == niMessageSize) { + return false; + } + + // Transaction-Id field. + byte[] niTransactionId = headers.getTextString(PduHeaders.TRANSACTION_ID); + if (null == niTransactionId) { + return false; + } + + break; + case PduHeaders.MESSAGE_TYPE_NOTIFYRESP_IND: + // Status field. + int nriStatus = headers.getOctet(PduHeaders.STATUS); + if (0 == nriStatus) { + return false; + } + + // Transaction-Id field. + byte[] nriTransactionId = headers.getTextString(PduHeaders.TRANSACTION_ID); + if (null == nriTransactionId) { + return false; + } + + break; + case PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF: + // Content-Type field. + byte[] rcContentType = headers.getTextString(PduHeaders.CONTENT_TYPE); + if (null == rcContentType) { + return false; + } + + // Date field. + long rcDate = headers.getLongInteger(PduHeaders.DATE); + if (-1 == rcDate) { + return false; + } + + break; + case PduHeaders.MESSAGE_TYPE_DELIVERY_IND: + // Date field. + long diDate = headers.getLongInteger(PduHeaders.DATE); + if (-1 == diDate) { + return false; + } + + // Message-Id field. + byte[] diMessageId = headers.getTextString(PduHeaders.MESSAGE_ID); + if (null == diMessageId) { + return false; + } + + // Status field. + int diStatus = headers.getOctet(PduHeaders.STATUS); + if (0 == diStatus) { + return false; + } + + // To field. + EncodedStringValue[] diTo = headers.getEncodedStringValues(PduHeaders.TO); + if (null == diTo) { + return false; + } + + break; + case PduHeaders.MESSAGE_TYPE_ACKNOWLEDGE_IND: + // Transaction-Id field. + byte[] aiTransactionId = headers.getTextString(PduHeaders.TRANSACTION_ID); + if (null == aiTransactionId) { + return false; + } + + break; + case PduHeaders.MESSAGE_TYPE_READ_ORIG_IND: + // Date field. + long roDate = headers.getLongInteger(PduHeaders.DATE); + if (-1 == roDate) { + return false; + } + + // From field. + EncodedStringValue roFrom = headers.getEncodedStringValue(PduHeaders.FROM); + if (null == roFrom) { + return false; + } + + // Message-Id field. + byte[] roMessageId = headers.getTextString(PduHeaders.MESSAGE_ID); + if (null == roMessageId) { + return false; + } + + // Read-Status field. + int roReadStatus = headers.getOctet(PduHeaders.READ_STATUS); + if (0 == roReadStatus) { + return false; + } + + // To field. + EncodedStringValue[] roTo = headers.getEncodedStringValues(PduHeaders.TO); + if (null == roTo) { + return false; + } + + break; + case PduHeaders.MESSAGE_TYPE_READ_REC_IND: + // From field. + EncodedStringValue rrFrom = headers.getEncodedStringValue(PduHeaders.FROM); + if (null == rrFrom) { + return false; + } + + // Message-Id field. + byte[] rrMessageId = headers.getTextString(PduHeaders.MESSAGE_ID); + if (null == rrMessageId) { + return false; + } + + // Read-Status field. + int rrReadStatus = headers.getOctet(PduHeaders.READ_STATUS); + if (0 == rrReadStatus) { + return false; + } + + // To field. + EncodedStringValue[] rrTo = headers.getEncodedStringValues(PduHeaders.TO); + if (null == rrTo) { + return false; + } + + break; + default: + // Parser doesn't support this message type in this version. + return false; + } + + return true; + } +} diff --git a/src/android/support/v7/mms/pdu/PduPart.java b/src/android/support/v7/mms/pdu/PduPart.java new file mode 100644 index 0000000..ec8e1a2 --- /dev/null +++ b/src/android/support/v7/mms/pdu/PduPart.java @@ -0,0 +1,414 @@ +/* + * Copyright (C) 2007-2008 Esmertec AG. + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +import android.net.Uri; + +import java.util.HashMap; +import java.util.Map; + +/** + * The pdu part. + */ +public class PduPart { + /** + * Well-Known Parameters. + */ + public static final int P_Q = 0x80; + public static final int P_CHARSET = 0x81; + public static final int P_LEVEL = 0x82; + public static final int P_TYPE = 0x83; + public static final int P_DEP_NAME = 0x85; + public static final int P_DEP_FILENAME = 0x86; + public static final int P_DIFFERENCES = 0x87; + public static final int P_PADDING = 0x88; + // This value of "TYPE" s used with Content-Type: multipart/related + public static final int P_CT_MR_TYPE = 0x89; + public static final int P_DEP_START = 0x8A; + public static final int P_DEP_START_INFO = 0x8B; + public static final int P_DEP_COMMENT = 0x8C; + public static final int P_DEP_DOMAIN = 0x8D; + public static final int P_MAX_AGE = 0x8E; + public static final int P_DEP_PATH = 0x8F; + public static final int P_SECURE = 0x90; + public static final int P_SEC = 0x91; + public static final int P_MAC = 0x92; + public static final int P_CREATION_DATE = 0x93; + public static final int P_MODIFICATION_DATE = 0x94; + public static final int P_READ_DATE = 0x95; + public static final int P_SIZE = 0x96; + public static final int P_NAME = 0x97; + public static final int P_FILENAME = 0x98; + public static final int P_START = 0x99; + public static final int P_START_INFO = 0x9A; + public static final int P_COMMENT = 0x9B; + public static final int P_DOMAIN = 0x9C; + public static final int P_PATH = 0x9D; + + /** + * Header field names. + */ + public static final int P_CONTENT_TYPE = 0x91; + public static final int P_CONTENT_LOCATION = 0x8E; + public static final int P_CONTENT_ID = 0xC0; + public static final int P_DEP_CONTENT_DISPOSITION = 0xAE; + public static final int P_CONTENT_DISPOSITION = 0xC5; + // The next header is unassigned header, use reserved header(0x48) value. + public static final int P_CONTENT_TRANSFER_ENCODING = 0xC8; + + /** + * Content=Transfer-Encoding string. + */ + public static final String CONTENT_TRANSFER_ENCODING = + "Content-Transfer-Encoding"; + + /** + * Value of Content-Transfer-Encoding. + */ + public static final String P_BINARY = "binary"; + public static final String P_7BIT = "7bit"; + public static final String P_8BIT = "8bit"; + public static final String P_BASE64 = "base64"; + public static final String P_QUOTED_PRINTABLE = "quoted-printable"; + + /** + * Value of disposition can be set to PduPart when the value is octet in + * the PDU. + * "from-data" instead of Form-data<Octet 128>. + * "attachment" instead of Attachment<Octet 129>. + * "inline" instead of Inline<Octet 130>. + */ + static final byte[] DISPOSITION_FROM_DATA = "from-data".getBytes(); + static final byte[] DISPOSITION_ATTACHMENT = "attachment".getBytes(); + static final byte[] DISPOSITION_INLINE = "inline".getBytes(); + + /** + * Content-Disposition value. + */ + public static final int P_DISPOSITION_FROM_DATA = 0x80; + public static final int P_DISPOSITION_ATTACHMENT = 0x81; + public static final int P_DISPOSITION_INLINE = 0x82; + + /** + * Header of part. + */ + private Map<Integer, Object> mPartHeader = null; + + /** + * Data uri. + */ + private Uri mUri = null; + + /** + * Part data. + */ + private byte[] mPartData = null; + + private static final String TAG = "PduPart"; + + /** + * Empty Constructor. + */ + public PduPart() { + mPartHeader = new HashMap<Integer, Object>(); + } + + /** + * Set part data. The data are stored as byte array. + * + * @param data the data + */ + public void setData(byte[] data) { + if(data == null) { + return; + } + + mPartData = new byte[data.length]; + System.arraycopy(data, 0, mPartData, 0, data.length); + } + + /** + * @return A copy of the part data or null if the data wasn't set or + * the data is stored as Uri. + * @see #getDataUri + */ + public byte[] getData() { + if(mPartData == null) { + return null; + } + + byte[] byteArray = new byte[mPartData.length]; + System.arraycopy(mPartData, 0, byteArray, 0, mPartData.length); + return byteArray; + } + + /** + * @return The length of the data, if this object have data, else 0. + */ + public int getDataLength() { + if(mPartData != null){ + return mPartData.length; + } else { + return 0; + } + } + + + /** + * Set data uri. The data are stored as Uri. + * + * @param uri the uri + */ + public void setDataUri(Uri uri) { + mUri = uri; + } + + /** + * @return The Uri of the part data or null if the data wasn't set or + * the data is stored as byte array. + * @see #getData + */ + public Uri getDataUri() { + return mUri; + } + + /** + * Set Content-id value + * + * @param contentId the content-id value + * @throws NullPointerException if the value is null. + */ + public void setContentId(byte[] contentId) { + if((contentId == null) || (contentId.length == 0)) { + throw new IllegalArgumentException( + "Content-Id may not be null or empty."); + } + + if ((contentId.length > 1) + && ((char) contentId[0] == '<') + && ((char) contentId[contentId.length - 1] == '>')) { + mPartHeader.put(P_CONTENT_ID, contentId); + return; + } + + // Insert beginning '<' and trailing '>' for Content-Id. + byte[] buffer = new byte[contentId.length + 2]; + buffer[0] = (byte) (0xff & '<'); + buffer[buffer.length - 1] = (byte) (0xff & '>'); + System.arraycopy(contentId, 0, buffer, 1, contentId.length); + mPartHeader.put(P_CONTENT_ID, buffer); + } + + /** + * Get Content-id value. + * + * @return the value + */ + public byte[] getContentId() { + return (byte[]) mPartHeader.get(P_CONTENT_ID); + } + + /** + * Set Char-set value. + * + * @param charset the value + */ + public void setCharset(int charset) { + mPartHeader.put(P_CHARSET, charset); + } + + /** + * Get Char-set value + * + * @return the charset value. Return 0 if charset was not set. + */ + public int getCharset() { + Integer charset = (Integer) mPartHeader.get(P_CHARSET); + if(charset == null) { + return 0; + } else { + return charset.intValue(); + } + } + + /** + * Set Content-Location value. + * + * @param contentLocation the value + * @throws NullPointerException if the value is null. + */ + public void setContentLocation(byte[] contentLocation) { + if(contentLocation == null) { + throw new NullPointerException("null content-location"); + } + + mPartHeader.put(P_CONTENT_LOCATION, contentLocation); + } + + /** + * Get Content-Location value. + * + * @return the value + * return PduPart.disposition[0] instead of <Octet 128> (Form-data). + * return PduPart.disposition[1] instead of <Octet 129> (Attachment). + * return PduPart.disposition[2] instead of <Octet 130> (Inline). + */ + public byte[] getContentLocation() { + return (byte[]) mPartHeader.get(P_CONTENT_LOCATION); + } + + /** + * Set Content-Disposition value. + * Use PduPart.disposition[0] instead of <Octet 128> (Form-data). + * Use PduPart.disposition[1] instead of <Octet 129> (Attachment). + * Use PduPart.disposition[2] instead of <Octet 130> (Inline). + * + * @param contentDisposition the value + * @throws NullPointerException if the value is null. + */ + public void setContentDisposition(byte[] contentDisposition) { + if(contentDisposition == null) { + throw new NullPointerException("null content-disposition"); + } + + mPartHeader.put(P_CONTENT_DISPOSITION, contentDisposition); + } + + /** + * Get Content-Disposition value. + * + * @return the value + */ + public byte[] getContentDisposition() { + return (byte[]) mPartHeader.get(P_CONTENT_DISPOSITION); + } + + /** + * Set Content-Type value. + * + * @param contentType the content type + * @throws NullPointerException if the value is null. + */ + public void setContentType(byte[] contentType) { + if(contentType == null) { + throw new NullPointerException("null content-type"); + } + + mPartHeader.put(P_CONTENT_TYPE, contentType); + } + + /** + * Get Content-Type value of part. + * + * @return the value + */ + public byte[] getContentType() { + return (byte[]) mPartHeader.get(P_CONTENT_TYPE); + } + + /** + * Set Content-Transfer-Encoding value + * + * @param contentTransferEncoding the Content-Transfer-Encoding value + * @throws NullPointerException if the value is null. + */ + public void setContentTransferEncoding(byte[] contentTransferEncoding) { + if(contentTransferEncoding == null) { + throw new NullPointerException("null content-transfer-encoding"); + } + + mPartHeader.put(P_CONTENT_TRANSFER_ENCODING, contentTransferEncoding); + } + + /** + * Get Content-Transfer-Encoding value. + * + * @return the value + */ + public byte[] getContentTransferEncoding() { + return (byte[]) mPartHeader.get(P_CONTENT_TRANSFER_ENCODING); + } + + /** + * Set Content-type parameter: name. + * + * @param name the name value + * @throws NullPointerException if the value is null. + */ + public void setName(byte[] name) { + if(null == name) { + throw new NullPointerException("null content-id"); + } + + mPartHeader.put(P_NAME, name); + } + + /** + * Get content-type parameter: name. + * + * @return the name + */ + public byte[] getName() { + return (byte[]) mPartHeader.get(P_NAME); + } + + /** + * Get Content-disposition parameter: filename + * + * @param fileName the filename value + * @throws NullPointerException if the value is null. + */ + public void setFilename(byte[] fileName) { + if(null == fileName) { + throw new NullPointerException("null content-id"); + } + + mPartHeader.put(P_FILENAME, fileName); + } + + /** + * Set Content-disposition parameter: filename + * + * @return the filename + */ + public byte[] getFilename() { + return (byte[]) mPartHeader.get(P_FILENAME); + } + + public String generateLocation() { + // Assumption: At least one of the content-location / name / filename + // or content-id should be set. This is guaranteed by the PduParser + // for incoming messages and by MM composer for outgoing messages. + byte[] location = (byte[]) mPartHeader.get(P_NAME); + if(null == location) { + location = (byte[]) mPartHeader.get(P_FILENAME); + + if (null == location) { + location = (byte[]) mPartHeader.get(P_CONTENT_LOCATION); + } + } + + if (null == location) { + byte[] contentId = (byte[]) mPartHeader.get(P_CONTENT_ID); + return "cid:" + new String(contentId); + } else { + return new String(location); + } + } +} + diff --git a/src/android/support/v7/mms/pdu/QuotedPrintable.java b/src/android/support/v7/mms/pdu/QuotedPrintable.java new file mode 100644 index 0000000..ea0abc5 --- /dev/null +++ b/src/android/support/v7/mms/pdu/QuotedPrintable.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +import java.io.ByteArrayOutputStream; + +public class QuotedPrintable { + private static byte ESCAPE_CHAR = '='; + + /** + * Decodes an array quoted-printable characters into an array of original bytes. + * Escaped characters are converted back to their original representation. + * + * <p> + * This function implements a subset of + * quoted-printable encoding specification (rule #1 and rule #2) + * as defined in RFC 1521. + * </p> + * + * @param bytes array of quoted-printable characters + * @return array of original bytes, + * null if quoted-printable decoding is unsuccessful. + */ + public static final byte[] decodeQuotedPrintable(byte[] bytes) { + if (bytes == null) { + return null; + } + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + for (int i = 0; i < bytes.length; i++) { + int b = bytes[i]; + if (b == ESCAPE_CHAR) { + try { + if('\r' == (char)bytes[i + 1] && + '\n' == (char)bytes[i + 2]) { + i += 2; + continue; + } + int u = Character.digit((char) bytes[++i], 16); + int l = Character.digit((char) bytes[++i], 16); + if (u == -1 || l == -1) { + return null; + } + buffer.write((char) ((u << 4) + l)); + } catch (ArrayIndexOutOfBoundsException e) { + return null; + } + } else { + buffer.write(b); + } + } + return buffer.toByteArray(); + } +} diff --git a/src/android/support/v7/mms/pdu/ReadOrigInd.java b/src/android/support/v7/mms/pdu/ReadOrigInd.java new file mode 100644 index 0000000..500a59d --- /dev/null +++ b/src/android/support/v7/mms/pdu/ReadOrigInd.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +public class ReadOrigInd extends GenericPdu { + /** + * Empty constructor. + * Since the Pdu corresponding to this class is constructed + * by the Proxy-Relay server, this class is only instantiated + * by the Pdu Parser. + * + * @throws InvalidHeaderValueException if error occurs. + */ + public ReadOrigInd() throws InvalidHeaderValueException { + super(); + setMessageType(PduHeaders.MESSAGE_TYPE_READ_ORIG_IND); + } + + /** + * Constructor with given headers. + * + * @param headers Headers for this PDU. + */ + ReadOrigInd(PduHeaders headers) { + super(headers); + } + + /** + * Get Date value. + * + * @return the value + */ + public long getDate() { + return mPduHeaders.getLongInteger(PduHeaders.DATE); + } + + /** + * Set Date value. + * + * @param value the value + */ + public void setDate(long value) { + mPduHeaders.setLongInteger(value, PduHeaders.DATE); + } + + /** + * Get From value. + * From-value = Value-length + * (Address-present-token Encoded-string-value | Insert-address-token) + * + * @return the value + */ + public EncodedStringValue getFrom() { + return mPduHeaders.getEncodedStringValue(PduHeaders.FROM); + } + + /** + * Set From value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setFrom(EncodedStringValue value) { + mPduHeaders.setEncodedStringValue(value, PduHeaders.FROM); + } + + /** + * Get Message-ID value. + * + * @return the value + */ + public byte[] getMessageId() { + return mPduHeaders.getTextString(PduHeaders.MESSAGE_ID); + } + + /** + * Set Message-ID value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setMessageId(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.MESSAGE_ID); + } + + /** + * Get X-MMS-Read-status value. + * + * @return the value + */ + public int getReadStatus() { + return mPduHeaders.getOctet(PduHeaders.READ_STATUS); + } + + /** + * Set X-MMS-Read-status value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + */ + public void setReadStatus(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.READ_STATUS); + } + + /** + * Get To value. + * + * @return the value + */ + public EncodedStringValue[] getTo() { + return mPduHeaders.getEncodedStringValues(PduHeaders.TO); + } + + /** + * Set To value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setTo(EncodedStringValue[] value) { + mPduHeaders.setEncodedStringValues(value, PduHeaders.TO); + } + + /* + * Optional, not supported header fields: + * + * public byte[] getApplicId() {return null;} + * public void setApplicId(byte[] value) {} + * + * public byte[] getAuxApplicId() {return null;} + * public void getAuxApplicId(byte[] value) {} + * + * public byte[] getReplyApplicId() {return 0x00;} + * public void setReplyApplicId(byte[] value) {} + */ +} diff --git a/src/android/support/v7/mms/pdu/ReadRecInd.java b/src/android/support/v7/mms/pdu/ReadRecInd.java new file mode 100644 index 0000000..9683d00 --- /dev/null +++ b/src/android/support/v7/mms/pdu/ReadRecInd.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +public class ReadRecInd extends GenericPdu { + /** + * Constructor, used when composing a M-ReadRec.ind pdu. + * + * @param from the from value + * @param messageId the message ID value + * @param mmsVersion current viersion of mms + * @param readStatus the read status value + * @param to the to value + * @throws InvalidHeaderValueException if parameters are invalid. + * NullPointerException if messageId or to is null. + */ + public ReadRecInd(EncodedStringValue from, + byte[] messageId, + int mmsVersion, + int readStatus, + EncodedStringValue[] to) throws InvalidHeaderValueException { + super(); + setMessageType(PduHeaders.MESSAGE_TYPE_READ_REC_IND); + setFrom(from); + setMessageId(messageId); + setMmsVersion(mmsVersion); + setTo(to); + setReadStatus(readStatus); + } + + /** + * Constructor with given headers. + * + * @param headers Headers for this PDU. + */ + ReadRecInd(PduHeaders headers) { + super(headers); + } + + /** + * Get Date value. + * + * @return the value + */ + public long getDate() { + return mPduHeaders.getLongInteger(PduHeaders.DATE); + } + + /** + * Set Date value. + * + * @param value the value + */ + public void setDate(long value) { + mPduHeaders.setLongInteger(value, PduHeaders.DATE); + } + + /** + * Get Message-ID value. + * + * @return the value + */ + public byte[] getMessageId() { + return mPduHeaders.getTextString(PduHeaders.MESSAGE_ID); + } + + /** + * Set Message-ID value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setMessageId(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.MESSAGE_ID); + } + + /** + * Get To value. + * + * @return the value + */ + public EncodedStringValue[] getTo() { + return mPduHeaders.getEncodedStringValues(PduHeaders.TO); + } + + /** + * Set To value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setTo(EncodedStringValue[] value) { + mPduHeaders.setEncodedStringValues(value, PduHeaders.TO); + } + + /** + * Get X-MMS-Read-status value. + * + * @return the value + */ + public int getReadStatus() { + return mPduHeaders.getOctet(PduHeaders.READ_STATUS); + } + + /** + * Set X-MMS-Read-status value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + */ + public void setReadStatus(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.READ_STATUS); + } + + /* + * Optional, not supported header fields: + * + * public byte[] getApplicId() {return null;} + * public void setApplicId(byte[] value) {} + * + * public byte[] getAuxApplicId() {return null;} + * public void getAuxApplicId(byte[] value) {} + * + * public byte[] getReplyApplicId() {return 0x00;} + * public void setReplyApplicId(byte[] value) {} + */ +} diff --git a/src/android/support/v7/mms/pdu/RetrieveConf.java b/src/android/support/v7/mms/pdu/RetrieveConf.java new file mode 100644 index 0000000..752ca04 --- /dev/null +++ b/src/android/support/v7/mms/pdu/RetrieveConf.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +/** + * M-Retrive.conf Pdu. + */ +public class RetrieveConf extends MultimediaMessagePdu { + /** + * Empty constructor. + * Since the Pdu corresponding to this class is constructed + * by the Proxy-Relay server, this class is only instantiated + * by the Pdu Parser. + * + * @throws InvalidHeaderValueException if error occurs. + */ + public RetrieveConf() throws InvalidHeaderValueException { + super(); + setMessageType(PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF); + } + + /** + * Constructor with given headers. + * + * @param headers Headers for this PDU. + */ + RetrieveConf(PduHeaders headers) { + super(headers); + } + + /** + * Constructor with given headers and body + * + * @param headers Headers for this PDU. + * @param body Body of this PDu. + */ + RetrieveConf(PduHeaders headers, PduBody body) { + super(headers, body); + } + + /** + * Get CC value. + * + * @return the value + */ + public EncodedStringValue[] getCc() { + return mPduHeaders.getEncodedStringValues(PduHeaders.CC); + } + + /** + * Add a "CC" value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void addCc(EncodedStringValue value) { + mPduHeaders.appendEncodedStringValue(value, PduHeaders.CC); + } + + /** + * Get Content-type value. + * + * @return the value + */ + public byte[] getContentType() { + return mPduHeaders.getTextString(PduHeaders.CONTENT_TYPE); + } + + /** + * Set Content-type value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setContentType(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.CONTENT_TYPE); + } + + /** + * Get X-Mms-Delivery-Report value. + * + * @return the value + */ + public int getDeliveryReport() { + return mPduHeaders.getOctet(PduHeaders.DELIVERY_REPORT); + } + + /** + * Set X-Mms-Delivery-Report value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + */ + public void setDeliveryReport(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.DELIVERY_REPORT); + } + + /** + * Get From value. + * From-value = Value-length + * (Address-present-token Encoded-string-value | Insert-address-token) + * + * @return the value + */ + public EncodedStringValue getFrom() { + return mPduHeaders.getEncodedStringValue(PduHeaders.FROM); + } + + /** + * Set From value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setFrom(EncodedStringValue value) { + mPduHeaders.setEncodedStringValue(value, PduHeaders.FROM); + } + + /** + * Get X-Mms-Message-Class value. + * Message-class-value = Class-identifier | Token-text + * Class-identifier = Personal | Advertisement | Informational | Auto + * + * @return the value + */ + public byte[] getMessageClass() { + return mPduHeaders.getTextString(PduHeaders.MESSAGE_CLASS); + } + + /** + * Set X-Mms-Message-Class value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setMessageClass(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.MESSAGE_CLASS); + } + + /** + * Get Message-ID value. + * + * @return the value + */ + public byte[] getMessageId() { + return mPduHeaders.getTextString(PduHeaders.MESSAGE_ID); + } + + /** + * Set Message-ID value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setMessageId(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.MESSAGE_ID); + } + + /** + * Get X-Mms-Read-Report value. + * + * @return the value + */ + public int getReadReport() { + return mPduHeaders.getOctet(PduHeaders.READ_REPORT); + } + + /** + * Set X-Mms-Read-Report value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + */ + public void setReadReport(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.READ_REPORT); + } + + /** + * Get X-Mms-Retrieve-Status value. + * + * @return the value + */ + public int getRetrieveStatus() { + return mPduHeaders.getOctet(PduHeaders.RETRIEVE_STATUS); + } + + /** + * Set X-Mms-Retrieve-Status value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + */ + public void setRetrieveStatus(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.RETRIEVE_STATUS); + } + + /** + * Get X-Mms-Retrieve-Text value. + * + * @return the value + */ + public EncodedStringValue getRetrieveText() { + return mPduHeaders.getEncodedStringValue(PduHeaders.RETRIEVE_TEXT); + } + + /** + * Set X-Mms-Retrieve-Text value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setRetrieveText(EncodedStringValue value) { + mPduHeaders.setEncodedStringValue(value, PduHeaders.RETRIEVE_TEXT); + } + + /** + * Get X-Mms-Transaction-Id. + * + * @return the value + */ + public byte[] getTransactionId() { + return mPduHeaders.getTextString(PduHeaders.TRANSACTION_ID); + } + + /** + * Set X-Mms-Transaction-Id. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setTransactionId(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.TRANSACTION_ID); + } + + /* + * Optional, not supported header fields: + * + * public byte[] getApplicId() {return null;} + * public void setApplicId(byte[] value) {} + * + * public byte[] getAuxApplicId() {return null;} + * public void getAuxApplicId(byte[] value) {} + * + * public byte getContentClass() {return 0x00;} + * public void setApplicId(byte value) {} + * + * public byte getDrmContent() {return 0x00;} + * public void setDrmContent(byte value) {} + * + * public byte getDistributionIndicator() {return 0x00;} + * public void setDistributionIndicator(byte value) {} + * + * public PreviouslySentByValue getPreviouslySentBy() {return null;} + * public void setPreviouslySentBy(PreviouslySentByValue value) {} + * + * public PreviouslySentDateValue getPreviouslySentDate() {} + * public void setPreviouslySentDate(PreviouslySentDateValue value) {} + * + * public MmFlagsValue getMmFlags() {return null;} + * public void setMmFlags(MmFlagsValue value) {} + * + * public MmStateValue getMmState() {return null;} + * public void getMmState(MmStateValue value) {} + * + * public byte[] getReplaceId() {return 0x00;} + * public void setReplaceId(byte[] value) {} + * + * public byte[] getReplyApplicId() {return 0x00;} + * public void setReplyApplicId(byte[] value) {} + * + * public byte getReplyCharging() {return 0x00;} + * public void setReplyCharging(byte value) {} + * + * public byte getReplyChargingDeadline() {return 0x00;} + * public void setReplyChargingDeadline(byte value) {} + * + * public byte[] getReplyChargingId() {return 0x00;} + * public void setReplyChargingId(byte[] value) {} + * + * public long getReplyChargingSize() {return 0;} + * public void setReplyChargingSize(long value) {} + */ +} diff --git a/src/android/support/v7/mms/pdu/SendConf.java b/src/android/support/v7/mms/pdu/SendConf.java new file mode 100644 index 0000000..5486398 --- /dev/null +++ b/src/android/support/v7/mms/pdu/SendConf.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2007 Esmertec AG. + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +public class SendConf extends GenericPdu { + /** + * Empty constructor. + * Since the Pdu corresponding to this class is constructed + * by the Proxy-Relay server, this class is only instantiated + * by the Pdu Parser. + * + * @throws InvalidHeaderValueException if error occurs. + */ + public SendConf() throws InvalidHeaderValueException { + super(); + setMessageType(PduHeaders.MESSAGE_TYPE_SEND_CONF); + } + + /** + * Constructor with given headers. + * + * @param headers Headers for this PDU. + */ + SendConf(PduHeaders headers) { + super(headers); + } + + /** + * Get Message-ID value. + * + * @return the value + */ + public byte[] getMessageId() { + return mPduHeaders.getTextString(PduHeaders.MESSAGE_ID); + } + + /** + * Set Message-ID value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setMessageId(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.MESSAGE_ID); + } + + /** + * Get X-Mms-Response-Status. + * + * @return the value + */ + public int getResponseStatus() { + return mPduHeaders.getOctet(PduHeaders.RESPONSE_STATUS); + } + + /** + * Set X-Mms-Response-Status. + * + * @param value the values + * @throws InvalidHeaderValueException if the value is invalid. + */ + public void setResponseStatus(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.RESPONSE_STATUS); + } + + /** + * Get X-Mms-Transaction-Id field value. + * + * @return the X-Mms-Report-Allowed value + */ + public byte[] getTransactionId() { + return mPduHeaders.getTextString(PduHeaders.TRANSACTION_ID); + } + + /** + * Set X-Mms-Transaction-Id field value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setTransactionId(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.TRANSACTION_ID); + } + + /* + * Optional, not supported header fields: + * + * public byte[] getContentLocation() {return null;} + * public void setContentLocation(byte[] value) {} + * + * public EncodedStringValue getResponseText() {return null;} + * public void setResponseText(EncodedStringValue value) {} + * + * public byte getStoreStatus() {return 0x00;} + * public void setStoreStatus(byte value) {} + * + * public byte[] getStoreStatusText() {return null;} + * public void setStoreStatusText(byte[] value) {} + */ +} diff --git a/src/android/support/v7/mms/pdu/SendReq.java b/src/android/support/v7/mms/pdu/SendReq.java new file mode 100644 index 0000000..0b78cc3 --- /dev/null +++ b/src/android/support/v7/mms/pdu/SendReq.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2007-2008 Esmertec AG. + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.mms.pdu; + +import android.util.Log; + +public class SendReq extends MultimediaMessagePdu { + private static final String TAG = "SendReq"; + + public SendReq() { + super(); + + try { + setMessageType(PduHeaders.MESSAGE_TYPE_SEND_REQ); + setMmsVersion(PduHeaders.CURRENT_MMS_VERSION); + // FIXME: Content-type must be decided according to whether + // SMIL part present. + setContentType("application/vnd.wap.multipart.related".getBytes()); + setFrom(new EncodedStringValue(PduHeaders.FROM_INSERT_ADDRESS_TOKEN_STR.getBytes())); + setTransactionId(generateTransactionId()); + } catch (InvalidHeaderValueException e) { + // Impossible to reach here since all headers we set above are valid. + Log.e(TAG, "Unexpected InvalidHeaderValueException.", e); + throw new RuntimeException(e); + } + } + + private byte[] generateTransactionId() { + String transactionId = "T" + Long.toHexString(System.currentTimeMillis()); + return transactionId.getBytes(); + } + + /** + * Constructor, used when composing a M-Send.req pdu. + * + * @param contentType the content type value + * @param from the from value + * @param mmsVersion current viersion of mms + * @param transactionId the transaction-id value + * @throws InvalidHeaderValueException if parameters are invalid. + * NullPointerException if contentType, form or transactionId is null. + */ + public SendReq(byte[] contentType, + EncodedStringValue from, + int mmsVersion, + byte[] transactionId) throws InvalidHeaderValueException { + super(); + setMessageType(PduHeaders.MESSAGE_TYPE_SEND_REQ); + setContentType(contentType); + setFrom(from); + setMmsVersion(mmsVersion); + setTransactionId(transactionId); + } + + /** + * Constructor with given headers. + * + * @param headers Headers for this PDU. + */ + SendReq(PduHeaders headers) { + super(headers); + } + + /** + * Constructor with given headers and body + * + * @param headers Headers for this PDU. + * @param body Body of this PDu. + */ + SendReq(PduHeaders headers, PduBody body) { + super(headers, body); + } + + /** + * Get Bcc value. + * + * @return the value + */ + public EncodedStringValue[] getBcc() { + return mPduHeaders.getEncodedStringValues(PduHeaders.BCC); + } + + /** + * Add a "BCC" value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void addBcc(EncodedStringValue value) { + mPduHeaders.appendEncodedStringValue(value, PduHeaders.BCC); + } + + /** + * Set "BCC" value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setBcc(EncodedStringValue[] value) { + mPduHeaders.setEncodedStringValues(value, PduHeaders.BCC); + } + + /** + * Get CC value. + * + * @return the value + */ + public EncodedStringValue[] getCc() { + return mPduHeaders.getEncodedStringValues(PduHeaders.CC); + } + + /** + * Add a "CC" value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void addCc(EncodedStringValue value) { + mPduHeaders.appendEncodedStringValue(value, PduHeaders.CC); + } + + /** + * Set "CC" value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setCc(EncodedStringValue[] value) { + mPduHeaders.setEncodedStringValues(value, PduHeaders.CC); + } + + /** + * Get Content-type value. + * + * @return the value + */ + public byte[] getContentType() { + return mPduHeaders.getTextString(PduHeaders.CONTENT_TYPE); + } + + /** + * Set Content-type value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setContentType(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.CONTENT_TYPE); + } + + /** + * Get X-Mms-Delivery-Report value. + * + * @return the value + */ + public int getDeliveryReport() { + return mPduHeaders.getOctet(PduHeaders.DELIVERY_REPORT); + } + + /** + * Set X-Mms-Delivery-Report value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + */ + public void setDeliveryReport(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.DELIVERY_REPORT); + } + + /** + * Get X-Mms-Expiry value. + * + * Expiry-value = Value-length + * (Absolute-token Date-value | Relative-token Delta-seconds-value) + * + * @return the value + */ + public long getExpiry() { + return mPduHeaders.getLongInteger(PduHeaders.EXPIRY); + } + + /** + * Set X-Mms-Expiry value. + * + * @param value the value + */ + public void setExpiry(long value) { + mPduHeaders.setLongInteger(value, PduHeaders.EXPIRY); + } + + /** + * Get X-Mms-MessageSize value. + * + * Expiry-value = size of message + * + * @return the value + */ + public long getMessageSize() { + return mPduHeaders.getLongInteger(PduHeaders.MESSAGE_SIZE); + } + + /** + * Set X-Mms-MessageSize value. + * + * @param value the value + */ + public void setMessageSize(long value) { + mPduHeaders.setLongInteger(value, PduHeaders.MESSAGE_SIZE); + } + + /** + * Get X-Mms-Message-Class value. + * Message-class-value = Class-identifier | Token-text + * Class-identifier = Personal | Advertisement | Informational | Auto + * + * @return the value + */ + public byte[] getMessageClass() { + return mPduHeaders.getTextString(PduHeaders.MESSAGE_CLASS); + } + + /** + * Set X-Mms-Message-Class value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setMessageClass(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.MESSAGE_CLASS); + } + + /** + * Get X-Mms-Read-Report value. + * + * @return the value + */ + public int getReadReport() { + return mPduHeaders.getOctet(PduHeaders.READ_REPORT); + } + + /** + * Set X-Mms-Read-Report value. + * + * @param value the value + * @throws InvalidHeaderValueException if the value is invalid. + */ + public void setReadReport(int value) throws InvalidHeaderValueException { + mPduHeaders.setOctet(value, PduHeaders.READ_REPORT); + } + + /** + * Set "To" value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setTo(EncodedStringValue[] value) { + mPduHeaders.setEncodedStringValues(value, PduHeaders.TO); + } + + /** + * Get X-Mms-Transaction-Id field value. + * + * @return the X-Mms-Report-Allowed value + */ + public byte[] getTransactionId() { + return mPduHeaders.getTextString(PduHeaders.TRANSACTION_ID); + } + + /** + * Set X-Mms-Transaction-Id field value. + * + * @param value the value + * @throws NullPointerException if the value is null. + */ + public void setTransactionId(byte[] value) { + mPduHeaders.setTextString(value, PduHeaders.TRANSACTION_ID); + } + + /* + * Optional, not supported header fields: + * + * public byte getAdaptationAllowed() {return 0}; + * public void setAdaptationAllowed(btye value) {}; + * + * public byte[] getApplicId() {return null;} + * public void setApplicId(byte[] value) {} + * + * public byte[] getAuxApplicId() {return null;} + * public void getAuxApplicId(byte[] value) {} + * + * public byte getContentClass() {return 0x00;} + * public void setApplicId(byte value) {} + * + * public long getDeliveryTime() {return 0}; + * public void setDeliveryTime(long value) {}; + * + * public byte getDrmContent() {return 0x00;} + * public void setDrmContent(byte value) {} + * + * public MmFlagsValue getMmFlags() {return null;} + * public void setMmFlags(MmFlagsValue value) {} + * + * public MmStateValue getMmState() {return null;} + * public void getMmState(MmStateValue value) {} + * + * public byte[] getReplyApplicId() {return 0x00;} + * public void setReplyApplicId(byte[] value) {} + * + * public byte getReplyCharging() {return 0x00;} + * public void setReplyCharging(byte value) {} + * + * public byte getReplyChargingDeadline() {return 0x00;} + * public void setReplyChargingDeadline(byte value) {} + * + * public byte[] getReplyChargingId() {return 0x00;} + * public void setReplyChargingId(byte[] value) {} + * + * public long getReplyChargingSize() {return 0;} + * public void setReplyChargingSize(long value) {} + * + * public byte[] getReplyApplicId() {return 0x00;} + * public void setReplyApplicId(byte[] value) {} + * + * public byte getStore() {return 0x00;} + * public void setStore(byte value) {} + */ +} |