diff options
author | Nathan Harold <nharold@google.com> | 2017-01-23 11:48:32 -0800 |
---|---|---|
committer | Nathan Harold <nharold@google.com> | 2017-01-23 11:48:32 -0800 |
commit | e48e7023599890fc0f7b2f8072d680d63479b3da (patch) | |
tree | 8289b25d31fb28745832589768bd9460573ec19c | |
parent | 358c00b8db24c6e7f33d0fa1bb24248926404b5c (diff) | |
parent | c001075c3bf556be1a55b333dad03c08c82a10e9 (diff) | |
download | base-stage-telephony-refactor.tar.gz |
Merge files from frameworks/opt/telephony to frameworks/basestage-telephony-refactor
Move files primarily related to SMS from opt to base to
fix two problems:
-frameworks/base should no longer depend on frameworks/opt
-Public APIs should always reside in frameworks/base
Bug: 33414487
Test: compilation
26 files changed, 15409 insertions, 0 deletions
diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java new file mode 100644 index 000000000000..8063364f2a3e --- /dev/null +++ b/telephony/java/android/telephony/SmsManager.java @@ -0,0 +1,1662 @@ +/* + * Copyright (C) 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.telephony; + +import android.app.ActivityThread; +import android.app.PendingIntent; +import android.content.ActivityNotFoundException; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.BaseBundle; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.telephony.IMms; +import com.android.internal.telephony.ISms; +import com.android.internal.telephony.SmsRawData; +import com.android.internal.telephony.uicc.IccConstants; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/* + * TODO(code review): Curious question... Why are a lot of these + * methods not declared as static, since they do not seem to require + * any local object state? Presumably this cannot be changed without + * interfering with the API... + */ + +/** + * Manages SMS operations such as sending data, text, and pdu SMS messages. + * Get this object by calling the static method {@link #getDefault()}. + * + * <p>For information about how to behave as the default SMS app on Android 4.4 (API level 19) + * and higher, see {@link android.provider.Telephony}. + */ +public final class SmsManager { + private static final String TAG = "SmsManager"; + /** + * A psuedo-subId that represents the default subId at any given time. The actual subId it + * represents changes as the default subId is changed. + */ + private static final int DEFAULT_SUBSCRIPTION_ID = -1002; + + /** Singleton object constructed during class initialization. */ + private static final SmsManager sInstance = new SmsManager(DEFAULT_SUBSCRIPTION_ID); + private static final Object sLockObject = new Object(); + + /** @hide */ + public static final int CELL_BROADCAST_RAN_TYPE_GSM = 0; + /** @hide */ + public static final int CELL_BROADCAST_RAN_TYPE_CDMA = 1; + + private static final Map<Integer, SmsManager> sSubInstances = + new ArrayMap<Integer, SmsManager>(); + + /** A concrete subscription id, or the pseudo DEFAULT_SUBSCRIPTION_ID */ + private int mSubId; + + /* + * Key for the various carrier-dependent configuration values. + * Some of the values are used by the system in processing SMS or MMS messages. Others + * are provided for the convenience of SMS applications. + */ + + /** + * Whether to append transaction id to MMS WAP Push M-Notification.ind's content location URI + * when constructing the download URL of a new MMS (boolean type) + */ + public static final String MMS_CONFIG_APPEND_TRANSACTION_ID = + CarrierConfigManager.KEY_MMS_APPEND_TRANSACTION_ID_BOOL; + /** + * Whether MMS is enabled for the current carrier (boolean type) + */ + public static final String + MMS_CONFIG_MMS_ENABLED = CarrierConfigManager.KEY_MMS_MMS_ENABLED_BOOL; + /** + * Whether group MMS is enabled for the current carrier (boolean type) + */ + public static final String + MMS_CONFIG_GROUP_MMS_ENABLED = CarrierConfigManager.KEY_MMS_GROUP_MMS_ENABLED_BOOL; + /** + * If this is enabled, M-NotifyResp.ind should be sent to the WAP Push content location instead + * of the default MMSC (boolean type) + */ + public static final String MMS_CONFIG_NOTIFY_WAP_MMSC_ENABLED = + CarrierConfigManager.KEY_MMS_NOTIFY_WAP_MMSC_ENABLED_BOOL; + /** + * Whether alias is enabled (boolean type) + */ + public static final String + MMS_CONFIG_ALIAS_ENABLED = CarrierConfigManager.KEY_MMS_ALIAS_ENABLED_BOOL; + /** + * Whether audio is allowed to be attached for MMS messages (boolean type) + */ + public static final String + MMS_CONFIG_ALLOW_ATTACH_AUDIO = CarrierConfigManager.KEY_MMS_ALLOW_ATTACH_AUDIO_BOOL; + /** + * Whether multipart SMS is enabled (boolean type) + */ + public static final String MMS_CONFIG_MULTIPART_SMS_ENABLED = + CarrierConfigManager.KEY_MMS_MULTIPART_SMS_ENABLED_BOOL; + /** + * Whether SMS delivery report is enabled (boolean type) + */ + public static final String MMS_CONFIG_SMS_DELIVERY_REPORT_ENABLED = + CarrierConfigManager.KEY_MMS_SMS_DELIVERY_REPORT_ENABLED_BOOL; + /** + * Whether content-disposition field should be expected in an MMS PDU (boolean type) + */ + public static final String MMS_CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION = + CarrierConfigManager.KEY_MMS_SUPPORT_MMS_CONTENT_DISPOSITION_BOOL; + /** + * Whether multipart SMS should be sent as separate messages + */ + public static final String MMS_CONFIG_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES = + CarrierConfigManager.KEY_MMS_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES_BOOL; + /** + * Whether MMS read report is enabled (boolean type) + */ + public static final String MMS_CONFIG_MMS_READ_REPORT_ENABLED = + CarrierConfigManager.KEY_MMS_MMS_READ_REPORT_ENABLED_BOOL; + /** + * Whether MMS delivery report is enabled (boolean type) + */ + public static final String MMS_CONFIG_MMS_DELIVERY_REPORT_ENABLED = + CarrierConfigManager.KEY_MMS_MMS_DELIVERY_REPORT_ENABLED_BOOL; + /** + * Max MMS message size in bytes (int type) + */ + public static final String + MMS_CONFIG_MAX_MESSAGE_SIZE = CarrierConfigManager.KEY_MMS_MAX_MESSAGE_SIZE_INT; + /** + * Max MMS image width (int type) + */ + public static final String + MMS_CONFIG_MAX_IMAGE_WIDTH = CarrierConfigManager.KEY_MMS_MAX_IMAGE_WIDTH_INT; + /** + * Max MMS image height (int type) + */ + public static final String + MMS_CONFIG_MAX_IMAGE_HEIGHT = CarrierConfigManager.KEY_MMS_MAX_IMAGE_HEIGHT_INT; + /** + * Limit of recipients of MMS messages (int type) + */ + public static final String + MMS_CONFIG_RECIPIENT_LIMIT = CarrierConfigManager.KEY_MMS_RECIPIENT_LIMIT_INT; + /** + * Min alias character count (int type) + */ + public static final String + MMS_CONFIG_ALIAS_MIN_CHARS = CarrierConfigManager.KEY_MMS_ALIAS_MIN_CHARS_INT; + /** + * Max alias character count (int type) + */ + public static final String + MMS_CONFIG_ALIAS_MAX_CHARS = CarrierConfigManager.KEY_MMS_ALIAS_MAX_CHARS_INT; + /** + * When the number of parts of a multipart SMS reaches this threshold, it should be converted + * into an MMS (int type) + */ + public static final String MMS_CONFIG_SMS_TO_MMS_TEXT_THRESHOLD = + CarrierConfigManager.KEY_MMS_SMS_TO_MMS_TEXT_THRESHOLD_INT; + /** + * Some carriers require SMS to be converted into MMS when text length reaches this threshold + * (int type) + */ + public static final String MMS_CONFIG_SMS_TO_MMS_TEXT_LENGTH_THRESHOLD = + CarrierConfigManager.KEY_MMS_SMS_TO_MMS_TEXT_LENGTH_THRESHOLD_INT; + /** + * Max message text size (int type) + */ + public static final String MMS_CONFIG_MESSAGE_TEXT_MAX_SIZE = + CarrierConfigManager.KEY_MMS_MESSAGE_TEXT_MAX_SIZE_INT; + /** + * Max message subject length (int type) + */ + public static final String + MMS_CONFIG_SUBJECT_MAX_LENGTH = CarrierConfigManager.KEY_MMS_SUBJECT_MAX_LENGTH_INT; + /** + * MMS HTTP socket timeout in milliseconds (int type) + */ + public static final String + MMS_CONFIG_HTTP_SOCKET_TIMEOUT = CarrierConfigManager.KEY_MMS_HTTP_SOCKET_TIMEOUT_INT; + /** + * The name of the UA Prof URL HTTP header for MMS HTTP request (String type) + */ + public static final String + MMS_CONFIG_UA_PROF_TAG_NAME = CarrierConfigManager.KEY_MMS_UA_PROF_TAG_NAME_STRING; + /** + * The User-Agent header value for MMS HTTP request (String type) + */ + public static final String + MMS_CONFIG_USER_AGENT = CarrierConfigManager.KEY_MMS_USER_AGENT_STRING; + /** + * The UA Profile URL header value for MMS HTTP request (String type) + */ + public static final String + MMS_CONFIG_UA_PROF_URL = CarrierConfigManager.KEY_MMS_UA_PROF_URL_STRING; + /** + * A list of HTTP headers to add to MMS HTTP request, separated by "|" (String type) + */ + public static final String + MMS_CONFIG_HTTP_PARAMS = CarrierConfigManager.KEY_MMS_HTTP_PARAMS_STRING; + /** + * Email gateway number (String type) + */ + public static final String MMS_CONFIG_EMAIL_GATEWAY_NUMBER = + CarrierConfigManager.KEY_MMS_EMAIL_GATEWAY_NUMBER_STRING; + /** + * The suffix to append to the NAI header value for MMS HTTP request (String type) + */ + public static final String + MMS_CONFIG_NAI_SUFFIX = CarrierConfigManager.KEY_MMS_NAI_SUFFIX_STRING; + /** + * If true, show the cell broadcast (amber alert) in the SMS settings. Some carriers don't want + * this shown. (Boolean type) + */ + public static final String MMS_CONFIG_SHOW_CELL_BROADCAST_APP_LINKS = + CarrierConfigManager.KEY_MMS_SHOW_CELL_BROADCAST_APP_LINKS_BOOL; + /** + * Whether the carrier MMSC supports charset field in Content-Type header. If this is false, + * then we don't add "charset" to "Content-Type" + */ + public static final String MMS_CONFIG_SUPPORT_HTTP_CHARSET_HEADER = + CarrierConfigManager.KEY_MMS_SUPPORT_HTTP_CHARSET_HEADER_BOOL; + /** + * If true, add "Connection: close" header to MMS HTTP requests so the connection + * is immediately closed (disabling keep-alive). (Boolean type) + * @hide + */ + public static final String MMS_CONFIG_CLOSE_CONNECTION = + CarrierConfigManager.KEY_MMS_CLOSE_CONNECTION_BOOL; + + /* + * Forwarded constants from SimDialogActivity. + */ + private static String DIALOG_TYPE_KEY = "dialog_type"; + private static final int SMS_PICK = 2; + + /** + * Send a text based SMS. + * + * <p class="note"><strong>Note:</strong> Using this method requires that your app has the + * {@link android.Manifest.permission#SEND_SMS} permission.</p> + * + * <p class="note"><strong>Note:</strong> Beginning with Android 4.4 (API level 19), if + * <em>and only if</em> an app is not selected as the default SMS app, the system automatically + * writes messages sent using this method to the SMS Provider (the default SMS app is always + * responsible for writing its sent messages to the SMS Provider). For information about + * how to behave as the default SMS app, see {@link android.provider.Telephony}.</p> + * + * + * @param destinationAddress the address to send the message to + * @param scAddress is the service center address or null to use + * the current default SMSC + * @param text the body of the message to send + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is successfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK</code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + * + * @throws IllegalArgumentException if destinationAddress or text are empty + */ + public void sendTextMessage( + String destinationAddress, String scAddress, String text, + PendingIntent sentIntent, PendingIntent deliveryIntent) { + sendTextMessageInternal(destinationAddress, scAddress, text, + sentIntent, deliveryIntent, true /* persistMessageForCarrierApp*/); + } + + private void sendTextMessageInternal(String destinationAddress, String scAddress, + String text, PendingIntent sentIntent, PendingIntent deliveryIntent, + boolean persistMessageForCarrierApp) { + if (TextUtils.isEmpty(destinationAddress)) { + throw new IllegalArgumentException("Invalid destinationAddress"); + } + + if (TextUtils.isEmpty(text)) { + throw new IllegalArgumentException("Invalid message body"); + } + + try { + ISms iccISms = getISmsServiceOrThrow(); + iccISms.sendTextForSubscriber(getSubscriptionId(), ActivityThread.currentPackageName(), + destinationAddress, + scAddress, text, sentIntent, deliveryIntent, + persistMessageForCarrierApp); + } catch (RemoteException ex) { + // ignore it + } + } + + /** + * Send a text based SMS without writing it into the SMS Provider. + * + * <p>Only the carrier app can call this method.</p> + * + * @see #sendTextMessage(String, String, String, PendingIntent, PendingIntent) + * @hide + */ + public void sendTextMessageWithoutPersisting( + String destinationAddress, String scAddress, String text, + PendingIntent sentIntent, PendingIntent deliveryIntent) { + sendTextMessageInternal(destinationAddress, scAddress, text, + sentIntent, deliveryIntent, false /* persistMessageForCarrierApp*/); + } + + /** + * A variant of {@link SmsManager#sendTextMessage} that allows self to be the caller. This is + * for internal use only. + * + * @param persistMessage whether to persist the sent message in the SMS app. the caller must be + * the Phone process if set to false. + * + * @hide + */ + public void sendTextMessageWithSelfPermissions( + String destinationAddress, String scAddress, String text, + PendingIntent sentIntent, PendingIntent deliveryIntent, boolean persistMessage) { + if (TextUtils.isEmpty(destinationAddress)) { + throw new IllegalArgumentException("Invalid destinationAddress"); + } + + if (TextUtils.isEmpty(text)) { + throw new IllegalArgumentException("Invalid message body"); + } + + try { + ISms iccISms = getISmsServiceOrThrow(); + iccISms.sendTextForSubscriberWithSelfPermissions(getSubscriptionId(), + ActivityThread.currentPackageName(), + destinationAddress, + scAddress, text, sentIntent, deliveryIntent, persistMessage); + } catch (RemoteException ex) { + // ignore it + } + } + + /** + * Inject an SMS PDU into the android application framework. + * + * The caller should have carrier privileges. + * @see android.telephony.TelephonyManager#hasCarrierPrivileges + * + * @param pdu is the byte array of pdu to be injected into android application framework + * @param format is the format of SMS pdu (3gpp or 3gpp2) + * @param receivedIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is successfully received by the + * android application framework, or failed. This intent is broadcasted at + * the same time an SMS received from radio is acknowledged back. + * The result code will be <code>RESULT_SMS_HANDLED</code> for success, or + * <code>RESULT_SMS_GENERIC_ERROR</code> for error. + * + * @throws IllegalArgumentException if format is not one of 3gpp and 3gpp2. + */ + public void injectSmsPdu(byte[] pdu, String format, PendingIntent receivedIntent) { + if (!format.equals(SmsMessage.FORMAT_3GPP) && !format.equals(SmsMessage.FORMAT_3GPP2)) { + // Format must be either 3gpp or 3gpp2. + throw new IllegalArgumentException( + "Invalid pdu format. format must be either 3gpp or 3gpp2"); + } + try { + ISms iccISms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + if (iccISms != null) { + iccISms.injectSmsPduForSubscriber( + getSubscriptionId(), pdu, format, receivedIntent); + } + } catch (RemoteException ex) { + // ignore it + } + } + + /** + * Divide a message text into several fragments, none bigger than + * the maximum SMS message size. + * + * @param text the original message. Must not be null. + * @return an <code>ArrayList</code> of strings that, in order, + * comprise the original message + * + * @throws IllegalArgumentException if text is null + */ + public ArrayList<String> divideMessage(String text) { + if (null == text) { + throw new IllegalArgumentException("text is null"); + } + return SmsMessage.fragmentText(text); + } + + /** + * Send a multi-part text based SMS. The callee should have already + * divided the message into correctly sized parts by calling + * <code>divideMessage</code>. + * + * <p class="note"><strong>Note:</strong> Using this method requires that your app has the + * {@link android.Manifest.permission#SEND_SMS} permission.</p> + * + * <p class="note"><strong>Note:</strong> Beginning with Android 4.4 (API level 19), if + * <em>and only if</em> an app is not selected as the default SMS app, the system automatically + * writes messages sent using this method to the SMS Provider (the default SMS app is always + * responsible for writing its sent messages to the SMS Provider). For information about + * how to behave as the default SMS app, see {@link android.provider.Telephony}.</p> + * + * @param destinationAddress the address to send the message to + * @param scAddress is the service center address or null to use + * the current default SMSC + * @param parts an <code>ArrayList</code> of strings that, in order, + * comprise the original message + * @param sentIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been sent. + * The result code will be <code>Activity.RESULT_OK</code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> each sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been delivered + * to the recipient. The raw pdu of the status report is in the + * extended data ("pdu"). + * + * @throws IllegalArgumentException if destinationAddress or data are empty + */ + public void sendMultipartTextMessage( + String destinationAddress, String scAddress, ArrayList<String> parts, + ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents) { + sendMultipartTextMessageInternal(destinationAddress, scAddress, parts, + sentIntents, deliveryIntents, true /* persistMessageForCarrierApp*/); + } + + private void sendMultipartTextMessageInternal( + String destinationAddress, String scAddress, ArrayList<String> parts, + ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents, + boolean persistMessageForCarrierApp) { + if (TextUtils.isEmpty(destinationAddress)) { + throw new IllegalArgumentException("Invalid destinationAddress"); + } + if (parts == null || parts.size() < 1) { + throw new IllegalArgumentException("Invalid message body"); + } + + if (parts.size() > 1) { + try { + ISms iccISms = getISmsServiceOrThrow(); + iccISms.sendMultipartTextForSubscriber(getSubscriptionId(), + ActivityThread.currentPackageName(), + destinationAddress, scAddress, parts, + sentIntents, deliveryIntents, persistMessageForCarrierApp); + } catch (RemoteException ex) { + // ignore it + } + } else { + PendingIntent sentIntent = null; + PendingIntent deliveryIntent = null; + if (sentIntents != null && sentIntents.size() > 0) { + sentIntent = sentIntents.get(0); + } + if (deliveryIntents != null && deliveryIntents.size() > 0) { + deliveryIntent = deliveryIntents.get(0); + } + sendTextMessage(destinationAddress, scAddress, parts.get(0), + sentIntent, deliveryIntent); + } + } + + /** + * Send a multi-part text based SMS without writing it into the SMS Provider. + * + * <p>Only the carrier app can call this method.</p> + * + * @see #sendMultipartTextMessage(String, String, ArrayList, ArrayList, ArrayList) + * @hide + **/ + public void sendMultipartTextMessageWithoutPersisting( + String destinationAddress, String scAddress, ArrayList<String> parts, + ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents) { + sendMultipartTextMessageInternal(destinationAddress, scAddress, parts, + sentIntents, deliveryIntents, false /* persistMessageForCarrierApp*/); + } + + /** + * Send a data based SMS to a specific application port. + * + * <p class="note"><strong>Note:</strong> Using this method requires that your app has the + * {@link android.Manifest.permission#SEND_SMS} permission.</p> + * + * @param destinationAddress the address to send the message to + * @param scAddress is the service center address or null to use + * the current default SMSC + * @param destinationPort the port to deliver the message to + * @param data the body of the message to send + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is successfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK</code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + * + * @throws IllegalArgumentException if destinationAddress or data are empty + */ + public void sendDataMessage( + String destinationAddress, String scAddress, short destinationPort, + byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) { + if (TextUtils.isEmpty(destinationAddress)) { + throw new IllegalArgumentException("Invalid destinationAddress"); + } + + if (data == null || data.length == 0) { + throw new IllegalArgumentException("Invalid message data"); + } + + try { + ISms iccISms = getISmsServiceOrThrow(); + iccISms.sendDataForSubscriber(getSubscriptionId(), ActivityThread.currentPackageName(), + destinationAddress, scAddress, destinationPort & 0xFFFF, + data, sentIntent, deliveryIntent); + } catch (RemoteException ex) { + // ignore it + } + } + + /** + * A variant of {@link SmsManager#sendDataMessage} that allows self to be the caller. This is + * for internal use only. + * + * @hide + */ + public void sendDataMessageWithSelfPermissions( + String destinationAddress, String scAddress, short destinationPort, + byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) { + if (TextUtils.isEmpty(destinationAddress)) { + throw new IllegalArgumentException("Invalid destinationAddress"); + } + + if (data == null || data.length == 0) { + throw new IllegalArgumentException("Invalid message data"); + } + + try { + ISms iccISms = getISmsServiceOrThrow(); + iccISms.sendDataForSubscriberWithSelfPermissions(getSubscriptionId(), + ActivityThread.currentPackageName(), destinationAddress, scAddress, + destinationPort & 0xFFFF, data, sentIntent, deliveryIntent); + } catch (RemoteException ex) { + // ignore it + } + } + + + + /** + * Get the SmsManager associated with the default subscription id. The instance will always be + * associated with the default subscription id, even if the default subscription id is changed. + * + * @return the SmsManager associated with the default subscription id + */ + public static SmsManager getDefault() { + return sInstance; + } + + /** + * Get the the instance of the SmsManager associated with a particular subscription id + * + * @param subId an SMS subscription id, typically accessed using + * {@link android.telephony.SubscriptionManager} + * @return the instance of the SmsManager associated with subId + */ + public static SmsManager getSmsManagerForSubscriptionId(int subId) { + // TODO(shri): Add javadoc link once SubscriptionManager is made public api + synchronized(sLockObject) { + SmsManager smsManager = sSubInstances.get(subId); + if (smsManager == null) { + smsManager = new SmsManager(subId); + sSubInstances.put(subId, smsManager); + } + return smsManager; + } + } + + private SmsManager(int subId) { + mSubId = subId; + } + + /** + * Get the associated subscription id. If the instance was returned by {@link #getDefault()}, + * then this method may return different values at different points in time (if the user + * changes the default subscription id). It will return < 0 if the default subscription id + * cannot be determined. + * + * Additionally, to support legacy applications that are not multi-SIM aware, + * if the following are true: + * - We are using a multi-SIM device + * - A default SMS SIM has not been selected + * - At least one SIM subscription is available + * then ask the user to set the default SMS SIM. + * + * @return associated subscription id + */ + public int getSubscriptionId() { + final int subId = (mSubId == DEFAULT_SUBSCRIPTION_ID) + ? getDefaultSmsSubscriptionId() : mSubId; + boolean isSmsSimPickActivityNeeded = false; + final Context context = ActivityThread.currentApplication().getApplicationContext(); + try { + ISms iccISms = getISmsService(); + if (iccISms != null) { + isSmsSimPickActivityNeeded = iccISms.isSmsSimPickActivityNeeded(subId); + } + } catch (RemoteException ex) { + Log.e(TAG, "Exception in getSubscriptionId"); + } + + if (isSmsSimPickActivityNeeded) { + Log.d(TAG, "getSubscriptionId isSmsSimPickActivityNeeded is true"); + // ask the user for a default SMS SIM. + Intent intent = new Intent(); + intent.setClassName("com.android.settings", + "com.android.settings.sim.SimDialogActivity"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(DIALOG_TYPE_KEY, SMS_PICK); + try { + context.startActivity(intent); + } catch (ActivityNotFoundException anfe) { + // If Settings is not installed, only log the error as we do not want to break + // legacy applications. + Log.e(TAG, "Unable to launch Settings application."); + } + } + + return subId; + } + + /** + * Returns the ISms service, or throws an UnsupportedOperationException if + * the service does not exist. + */ + private static ISms getISmsServiceOrThrow() { + ISms iccISms = getISmsService(); + if (iccISms == null) { + throw new UnsupportedOperationException("Sms is not supported"); + } + return iccISms; + } + + private static ISms getISmsService() { + return ISms.Stub.asInterface(ServiceManager.getService("isms")); + } + + /** + * Copy a raw SMS PDU to the ICC. + * ICC (Integrated Circuit Card) is the card of the device. + * For example, this can be the SIM or USIM for GSM. + * + * @param smsc the SMSC for this message, or NULL for the default SMSC + * @param pdu the raw PDU to store + * @param status message status (STATUS_ON_ICC_READ, STATUS_ON_ICC_UNREAD, + * STATUS_ON_ICC_SENT, STATUS_ON_ICC_UNSENT) + * @return true for success + * + * @throws IllegalArgumentException if pdu is NULL + * {@hide} + */ + public boolean copyMessageToIcc(byte[] smsc, byte[] pdu,int status) { + boolean success = false; + + if (null == pdu) { + throw new IllegalArgumentException("pdu is NULL"); + } + try { + ISms iccISms = getISmsService(); + if (iccISms != null) { + success = iccISms.copyMessageToIccEfForSubscriber(getSubscriptionId(), + ActivityThread.currentPackageName(), + status, pdu, smsc); + } + } catch (RemoteException ex) { + // ignore it + } + + return success; + } + + /** + * Delete the specified message from the ICC. + * ICC (Integrated Circuit Card) is the card of the device. + * For example, this can be the SIM or USIM for GSM. + * + * @param messageIndex is the record index of the message on ICC + * @return true for success + * + * {@hide} + */ + public boolean + deleteMessageFromIcc(int messageIndex) { + boolean success = false; + byte[] pdu = new byte[IccConstants.SMS_RECORD_LENGTH-1]; + Arrays.fill(pdu, (byte)0xff); + + try { + ISms iccISms = getISmsService(); + if (iccISms != null) { + success = iccISms.updateMessageOnIccEfForSubscriber(getSubscriptionId(), + ActivityThread.currentPackageName(), + messageIndex, STATUS_ON_ICC_FREE, pdu); + } + } catch (RemoteException ex) { + // ignore it + } + + return success; + } + + /** + * Update the specified message on the ICC. + * ICC (Integrated Circuit Card) is the card of the device. + * For example, this can be the SIM or USIM for GSM. + * + * @param messageIndex record index of message to update + * @param newStatus new message status (STATUS_ON_ICC_READ, + * STATUS_ON_ICC_UNREAD, STATUS_ON_ICC_SENT, + * STATUS_ON_ICC_UNSENT, STATUS_ON_ICC_FREE) + * @param pdu the raw PDU to store + * @return true for success + * + * {@hide} + */ + public boolean updateMessageOnIcc(int messageIndex, int newStatus, byte[] pdu) { + boolean success = false; + + try { + ISms iccISms = getISmsService(); + if (iccISms != null) { + success = iccISms.updateMessageOnIccEfForSubscriber(getSubscriptionId(), + ActivityThread.currentPackageName(), + messageIndex, newStatus, pdu); + } + } catch (RemoteException ex) { + // ignore it + } + + return success; + } + + /** + * Retrieves all messages currently stored on ICC. + * ICC (Integrated Circuit Card) is the card of the device. + * For example, this can be the SIM or USIM for GSM. + * + * @return <code>ArrayList</code> of <code>SmsMessage</code> objects + * + * {@hide} + */ + public ArrayList<SmsMessage> getAllMessagesFromIcc() { + List<SmsRawData> records = null; + + try { + ISms iccISms = getISmsService(); + if (iccISms != null) { + records = iccISms.getAllMessagesFromIccEfForSubscriber( + getSubscriptionId(), + ActivityThread.currentPackageName()); + } + } catch (RemoteException ex) { + // ignore it + } + + return createMessageListFromRawRecords(records); + } + + /** + * Enable reception of cell broadcast (SMS-CB) messages with the given + * message identifier and RAN type. The RAN type specify this message ID + * belong to 3GPP (GSM) or 3GPP2(CDMA).Note that if two different clients + * enable the same message identifier, they must both disable it for the device to stop + * receiving those messages. All received messages will be broadcast in an + * intent with the action "android.provider.Telephony.SMS_CB_RECEIVED". + * Note: This call is blocking, callers may want to avoid calling it from + * the main thread of an application. + * + * @param messageIdentifier Message identifier as specified in TS 23.041 (3GPP) + * or C.R1001-G (3GPP2) + * @param ranType as defined in class SmsManager, the value can be one of these: + * android.telephony.SmsMessage.CELL_BROADCAST_RAN_TYPE_GSM + * android.telephony.SmsMessage.CELL_BROADCAST_RAN_TYPE_CDMA + * @return true if successful, false otherwise + * @see #disableCellBroadcast(int, int) + * + * {@hide} + */ + public boolean enableCellBroadcast(int messageIdentifier, int ranType) { + boolean success = false; + + try { + ISms iccISms = getISmsService(); + if (iccISms != null) { + success = iccISms.enableCellBroadcastForSubscriber( + getSubscriptionId(), messageIdentifier, ranType); + } + } catch (RemoteException ex) { + // ignore it + } + + return success; + } + + /** + * Disable reception of cell broadcast (SMS-CB) messages with the given + * message identifier and RAN type. The RAN type specify this message ID + * belong to 3GPP (GSM) or 3GPP2(CDMA). Note that if two different clients + * enable the same message identifier, they must both disable it for the + * device to stop receiving those messages. + * Note: This call is blocking, callers may want to avoid calling it from + * the main thread of an application. + * + * @param messageIdentifier Message identifier as specified in TS 23.041 (3GPP) + * or C.R1001-G (3GPP2) + * @param ranType as defined in class SmsManager, the value can be one of these: + * android.telephony.SmsMessage.CELL_BROADCAST_RAN_TYPE_GSM + * android.telephony.SmsMessage.CELL_BROADCAST_RAN_TYPE_CDMA + * @return true if successful, false otherwise + * + * @see #enableCellBroadcast(int, int) + * + * {@hide} + */ + public boolean disableCellBroadcast(int messageIdentifier, int ranType) { + boolean success = false; + + try { + ISms iccISms = getISmsService(); + if (iccISms != null) { + success = iccISms.disableCellBroadcastForSubscriber( + getSubscriptionId(), messageIdentifier, ranType); + } + } catch (RemoteException ex) { + // ignore it + } + + return success; + } + + /** + * Enable reception of cell broadcast (SMS-CB) messages with the given + * message identifier range and RAN type. The RAN type specify this message ID + * belong to 3GPP (GSM) or 3GPP2(CDMA). Note that if two different clients enable + * the same message identifier, they must both disable it for the device to stop + * receiving those messages. All received messages will be broadcast in an + * intent with the action "android.provider.Telephony.SMS_CB_RECEIVED". + * Note: This call is blocking, callers may want to avoid calling it from + * the main thread of an application. + * + * @param startMessageId first message identifier as specified in TS 23.041 (3GPP) + * or C.R1001-G (3GPP2) + * @param endMessageId last message identifier as specified in TS 23.041 (3GPP) + * or C.R1001-G (3GPP2) + * @param ranType as defined in class SmsManager, the value can be one of these: + * android.telephony.SmsMessage.CELL_BROADCAST_RAN_TYPE_GSM + * android.telephony.SmsMessage.CELL_BROADCAST_RAN_TYPE_CDMA + * @return true if successful, false otherwise + * @see #disableCellBroadcastRange(int, int, int) + * + * @throws IllegalArgumentException if endMessageId < startMessageId + * {@hide} + */ + public boolean enableCellBroadcastRange(int startMessageId, int endMessageId, int ranType) { + boolean success = false; + + if (endMessageId < startMessageId) { + throw new IllegalArgumentException("endMessageId < startMessageId"); + } + try { + ISms iccISms = getISmsService(); + if (iccISms != null) { + success = iccISms.enableCellBroadcastRangeForSubscriber(getSubscriptionId(), + startMessageId, endMessageId, ranType); + } + } catch (RemoteException ex) { + // ignore it + } + + return success; + } + + /** + * Disable reception of cell broadcast (SMS-CB) messages with the given + * message identifier range and RAN type. The RAN type specify this message + * ID range belong to 3GPP (GSM) or 3GPP2(CDMA). Note that if two different + * clients enable the same message identifier, they must both disable it for + * the device to stop receiving those messages. + * Note: This call is blocking, callers may want to avoid calling it from + * the main thread of an application. + * + * @param startMessageId first message identifier as specified in TS 23.041 (3GPP) + * or C.R1001-G (3GPP2) + * @param endMessageId last message identifier as specified in TS 23.041 (3GPP) + * or C.R1001-G (3GPP2) + * @param ranType as defined in class SmsManager, the value can be one of these: + * android.telephony.SmsMessage.CELL_BROADCAST_RAN_TYPE_GSM + * android.telephony.SmsMessage.CELL_BROADCAST_RAN_TYPE_CDMA + * @return true if successful, false otherwise + * + * @see #enableCellBroadcastRange(int, int, int) + * + * @throws IllegalArgumentException if endMessageId < startMessageId + * {@hide} + */ + public boolean disableCellBroadcastRange(int startMessageId, int endMessageId, int ranType) { + boolean success = false; + + if (endMessageId < startMessageId) { + throw new IllegalArgumentException("endMessageId < startMessageId"); + } + try { + ISms iccISms = getISmsService(); + if (iccISms != null) { + success = iccISms.disableCellBroadcastRangeForSubscriber(getSubscriptionId(), + startMessageId, endMessageId, ranType); + } + } catch (RemoteException ex) { + // ignore it + } + + return success; + } + + /** + * Create a list of <code>SmsMessage</code>s from a list of RawSmsData + * records returned by <code>getAllMessagesFromIcc()</code> + * + * @param records SMS EF records, returned by + * <code>getAllMessagesFromIcc</code> + * @return <code>ArrayList</code> of <code>SmsMessage</code> objects. + */ + private static ArrayList<SmsMessage> createMessageListFromRawRecords(List<SmsRawData> records) { + ArrayList<SmsMessage> messages = new ArrayList<SmsMessage>(); + if (records != null) { + int count = records.size(); + for (int i = 0; i < count; i++) { + SmsRawData data = records.get(i); + // List contains all records, including "free" records (null) + if (data != null) { + SmsMessage sms = SmsMessage.createFromEfRecord(i+1, data.getBytes()); + if (sms != null) { + messages.add(sms); + } + } + } + } + return messages; + } + + /** + * SMS over IMS is supported if IMS is registered and SMS is supported + * on IMS. + * + * @return true if SMS over IMS is supported, false otherwise + * + * @see #getImsSmsFormat() + * + * @hide + */ + public boolean isImsSmsSupported() { + boolean boSupported = false; + try { + ISms iccISms = getISmsService(); + if (iccISms != null) { + boSupported = iccISms.isImsSmsSupportedForSubscriber(getSubscriptionId()); + } + } catch (RemoteException ex) { + // ignore it + } + return boSupported; + } + + /** + * Gets SMS format supported on IMS. SMS over IMS format is + * either 3GPP or 3GPP2. + * + * @return SmsMessage.FORMAT_3GPP, + * SmsMessage.FORMAT_3GPP2 + * or SmsMessage.FORMAT_UNKNOWN + * + * @see #isImsSmsSupported() + * + * @hide + */ + public String getImsSmsFormat() { + String format = com.android.internal.telephony.SmsConstants.FORMAT_UNKNOWN; + try { + ISms iccISms = getISmsService(); + if (iccISms != null) { + format = iccISms.getImsSmsFormatForSubscriber(getSubscriptionId()); + } + } catch (RemoteException ex) { + // ignore it + } + return format; + } + + /** + * Get default sms subscription id + * + * @return the default SMS subscription id + */ + public static int getDefaultSmsSubscriptionId() { + ISms iccISms = null; + try { + iccISms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + return iccISms.getPreferredSmsSubscription(); + } catch (RemoteException ex) { + return -1; + } catch (NullPointerException ex) { + return -1; + } + } + + /** + * Get SMS prompt property, enabled or not + * + * @return true if enabled, false otherwise + * @hide + */ + public boolean isSMSPromptEnabled() { + ISms iccISms = null; + try { + iccISms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + return iccISms.isSMSPromptEnabled(); + } catch (RemoteException ex) { + return false; + } catch (NullPointerException ex) { + return false; + } + } + + // see SmsMessage.getStatusOnIcc + + /** Free space (TS 51.011 10.5.3 / 3GPP2 C.S0023 3.4.27). */ + static public final int STATUS_ON_ICC_FREE = 0; + + /** Received and read (TS 51.011 10.5.3 / 3GPP2 C.S0023 3.4.27). */ + static public final int STATUS_ON_ICC_READ = 1; + + /** Received and unread (TS 51.011 10.5.3 / 3GPP2 C.S0023 3.4.27). */ + static public final int STATUS_ON_ICC_UNREAD = 3; + + /** Stored and sent (TS 51.011 10.5.3 / 3GPP2 C.S0023 3.4.27). */ + static public final int STATUS_ON_ICC_SENT = 5; + + /** Stored and unsent (TS 51.011 10.5.3 / 3GPP2 C.S0023 3.4.27). */ + static public final int STATUS_ON_ICC_UNSENT = 7; + + // SMS send failure result codes + + /** Generic failure cause */ + static public final int RESULT_ERROR_GENERIC_FAILURE = 1; + /** Failed because radio was explicitly turned off */ + static public final int RESULT_ERROR_RADIO_OFF = 2; + /** Failed because no pdu provided */ + static public final int RESULT_ERROR_NULL_PDU = 3; + /** Failed because service is currently unavailable */ + static public final int RESULT_ERROR_NO_SERVICE = 4; + /** Failed because we reached the sending queue limit. {@hide} */ + static public final int RESULT_ERROR_LIMIT_EXCEEDED = 5; + /** Failed because FDN is enabled. {@hide} */ + static public final int RESULT_ERROR_FDN_CHECK_FAILURE = 6; + + static private final String PHONE_PACKAGE_NAME = "com.android.phone"; + + /** + * Send an MMS message + * + * @param context application context + * @param contentUri the content Uri from which the message pdu will be read + * @param locationUrl the optional location url where message should be sent to + * @param configOverrides the carrier-specific messaging configuration values to override for + * sending the message. + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is successfully sent, or failed + * @throws IllegalArgumentException if contentUri is empty + */ + public void sendMultimediaMessage(Context context, Uri contentUri, String locationUrl, + Bundle configOverrides, PendingIntent sentIntent) { + if (contentUri == null) { + throw new IllegalArgumentException("Uri contentUri null"); + } + try { + final IMms iMms = IMms.Stub.asInterface(ServiceManager.getService("imms")); + if (iMms == null) { + return; + } + + iMms.sendMessage(getSubscriptionId(), ActivityThread.currentPackageName(), contentUri, + locationUrl, configOverrides, sentIntent); + } catch (RemoteException e) { + // Ignore it + } + } + + /** + * Download an MMS message from carrier by a given location URL + * + * @param context application context + * @param locationUrl the location URL of the MMS message to be downloaded, usually obtained + * from the MMS WAP push notification + * @param contentUri the content uri to which the downloaded pdu will be written + * @param configOverrides the carrier-specific messaging configuration values to override for + * downloading the message. + * @param downloadedIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is downloaded, or the download is failed + * @throws IllegalArgumentException if locationUrl or contentUri is empty + */ + public void downloadMultimediaMessage(Context context, String locationUrl, Uri contentUri, + Bundle configOverrides, PendingIntent downloadedIntent) { + if (TextUtils.isEmpty(locationUrl)) { + throw new IllegalArgumentException("Empty MMS location URL"); + } + if (contentUri == null) { + throw new IllegalArgumentException("Uri contentUri null"); + } + try { + final IMms iMms = IMms.Stub.asInterface(ServiceManager.getService("imms")); + if (iMms == null) { + return; + } + iMms.downloadMessage( + getSubscriptionId(), ActivityThread.currentPackageName(), locationUrl, + contentUri, configOverrides, downloadedIntent); + } catch (RemoteException e) { + // Ignore it + } + } + + // MMS send/download failure result codes + public static final int MMS_ERROR_UNSPECIFIED = 1; + public static final int MMS_ERROR_INVALID_APN = 2; + public static final int MMS_ERROR_UNABLE_CONNECT_MMS = 3; + public static final int MMS_ERROR_HTTP_FAILURE = 4; + public static final int MMS_ERROR_IO_ERROR = 5; + public static final int MMS_ERROR_RETRY = 6; + public static final int MMS_ERROR_CONFIGURATION_ERROR = 7; + public static final int MMS_ERROR_NO_DATA_NETWORK = 8; + + /** Intent extra name for MMS sending result data in byte array type */ + public static final String EXTRA_MMS_DATA = "android.telephony.extra.MMS_DATA"; + /** Intent extra name for HTTP status code for MMS HTTP failure in integer type */ + public static final String EXTRA_MMS_HTTP_STATUS = "android.telephony.extra.MMS_HTTP_STATUS"; + + /** + * Import a text message into system's SMS store + * + * Only default SMS apps can import SMS + * + * @param address the destination(source) address of the sent(received) message + * @param type the type of the message + * @param text the message text + * @param timestampMillis the message timestamp in milliseconds + * @param seen if the message is seen + * @param read if the message is read + * @return the message URI, null if failed + * @hide + */ + public Uri importTextMessage(String address, int type, String text, long timestampMillis, + boolean seen, boolean read) { + try { + IMms iMms = IMms.Stub.asInterface(ServiceManager.getService("imms")); + if (iMms != null) { + return iMms.importTextMessage(ActivityThread.currentPackageName(), + address, type, text, timestampMillis, seen, read); + } + } catch (RemoteException ex) { + // ignore it + } + return null; + } + + /** Represents the received SMS message for importing {@hide} */ + public static final int SMS_TYPE_INCOMING = 0; + /** Represents the sent SMS message for importing {@hide} */ + public static final int SMS_TYPE_OUTGOING = 1; + + /** + * Import a multimedia message into system's MMS store. Only the following PDU type is + * supported: Retrieve.conf, Send.req, Notification.ind, Delivery.ind, Read-Orig.ind + * + * Only default SMS apps can import MMS + * + * @param contentUri the content uri from which to read the PDU of the message to import + * @param messageId the optional message id. Use null if not specifying + * @param timestampSecs the optional message timestamp. Use -1 if not specifying + * @param seen if the message is seen + * @param read if the message is read + * @return the message URI, null if failed + * @throws IllegalArgumentException if pdu is empty + * {@hide} + */ + public Uri importMultimediaMessage(Uri contentUri, String messageId, long timestampSecs, + boolean seen, boolean read) { + if (contentUri == null) { + throw new IllegalArgumentException("Uri contentUri null"); + } + try { + IMms iMms = IMms.Stub.asInterface(ServiceManager.getService("imms")); + if (iMms != null) { + return iMms.importMultimediaMessage(ActivityThread.currentPackageName(), + contentUri, messageId, timestampSecs, seen, read); + } + } catch (RemoteException ex) { + // ignore it + } + return null; + } + + /** + * Delete a system stored SMS or MMS message + * + * Only default SMS apps can delete system stored SMS and MMS messages + * + * @param messageUri the URI of the stored message + * @return true if deletion is successful, false otherwise + * @throws IllegalArgumentException if messageUri is empty + * {@hide} + */ + public boolean deleteStoredMessage(Uri messageUri) { + if (messageUri == null) { + throw new IllegalArgumentException("Empty message URI"); + } + try { + IMms iMms = IMms.Stub.asInterface(ServiceManager.getService("imms")); + if (iMms != null) { + return iMms.deleteStoredMessage(ActivityThread.currentPackageName(), messageUri); + } + } catch (RemoteException ex) { + // ignore it + } + return false; + } + + /** + * Delete a system stored SMS or MMS thread + * + * Only default SMS apps can delete system stored SMS and MMS conversations + * + * @param conversationId the ID of the message conversation + * @return true if deletion is successful, false otherwise + * {@hide} + */ + public boolean deleteStoredConversation(long conversationId) { + try { + IMms iMms = IMms.Stub.asInterface(ServiceManager.getService("imms")); + if (iMms != null) { + return iMms.deleteStoredConversation( + ActivityThread.currentPackageName(), conversationId); + } + } catch (RemoteException ex) { + // ignore it + } + return false; + } + + /** + * Update the status properties of a system stored SMS or MMS message, e.g. + * the read status of a message, etc. + * + * @param messageUri the URI of the stored message + * @param statusValues a list of status properties in key-value pairs to update + * @return true if update is successful, false otherwise + * @throws IllegalArgumentException if messageUri is empty + * {@hide} + */ + public boolean updateStoredMessageStatus(Uri messageUri, ContentValues statusValues) { + if (messageUri == null) { + throw new IllegalArgumentException("Empty message URI"); + } + try { + IMms iMms = IMms.Stub.asInterface(ServiceManager.getService("imms")); + if (iMms != null) { + return iMms.updateStoredMessageStatus(ActivityThread.currentPackageName(), + messageUri, statusValues); + } + } catch (RemoteException ex) { + // ignore it + } + return false; + } + + /** Message status property: whether the message has been seen. 1 means seen, 0 not {@hide} */ + public static final String MESSAGE_STATUS_SEEN = "seen"; + /** Message status property: whether the message has been read. 1 means read, 0 not {@hide} */ + public static final String MESSAGE_STATUS_READ = "read"; + + /** + * Archive or unarchive a stored conversation + * + * @param conversationId the ID of the message conversation + * @param archived true to archive the conversation, false to unarchive + * @return true if update is successful, false otherwise + * {@hide} + */ + public boolean archiveStoredConversation(long conversationId, boolean archived) { + try { + IMms iMms = IMms.Stub.asInterface(ServiceManager.getService("imms")); + if (iMms != null) { + return iMms.archiveStoredConversation(ActivityThread.currentPackageName(), + conversationId, archived); + } + } catch (RemoteException ex) { + // ignore it + } + return false; + } + + /** + * Add a text message draft to system SMS store + * + * Only default SMS apps can add SMS draft + * + * @param address the destination address of message + * @param text the body of the message to send + * @return the URI of the stored draft message + * {@hide} + */ + public Uri addTextMessageDraft(String address, String text) { + try { + IMms iMms = IMms.Stub.asInterface(ServiceManager.getService("imms")); + if (iMms != null) { + return iMms.addTextMessageDraft(ActivityThread.currentPackageName(), address, text); + } + } catch (RemoteException ex) { + // ignore it + } + return null; + } + + /** + * Add a multimedia message draft to system MMS store + * + * Only default SMS apps can add MMS draft + * + * @param contentUri the content uri from which to read the PDU data of the draft MMS + * @return the URI of the stored draft message + * @throws IllegalArgumentException if pdu is empty + * {@hide} + */ + public Uri addMultimediaMessageDraft(Uri contentUri) { + if (contentUri == null) { + throw new IllegalArgumentException("Uri contentUri null"); + } + try { + IMms iMms = IMms.Stub.asInterface(ServiceManager.getService("imms")); + if (iMms != null) { + return iMms.addMultimediaMessageDraft(ActivityThread.currentPackageName(), + contentUri); + } + } catch (RemoteException ex) { + // ignore it + } + return null; + } + + /** + * Send a system stored text message. + * + * You can only send a failed text message or a draft text message. + * + * @param messageUri the URI of the stored message + * @param scAddress is the service center address or null to use the current default SMSC + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is successfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK</code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + * + * @throws IllegalArgumentException if messageUri is empty + * {@hide} + */ + public void sendStoredTextMessage(Uri messageUri, String scAddress, PendingIntent sentIntent, + PendingIntent deliveryIntent) { + if (messageUri == null) { + throw new IllegalArgumentException("Empty message URI"); + } + try { + ISms iccISms = getISmsServiceOrThrow(); + iccISms.sendStoredText( + getSubscriptionId(), ActivityThread.currentPackageName(), messageUri, + scAddress, sentIntent, deliveryIntent); + } catch (RemoteException ex) { + // ignore it + } + } + + /** + * Send a system stored multi-part text message. + * + * You can only send a failed text message or a draft text message. + * The provided <code>PendingIntent</code> lists should match the part number of the + * divided text of the stored message by using <code>divideMessage</code> + * + * @param messageUri the URI of the stored message + * @param scAddress is the service center address or null to use + * the current default SMSC + * @param sentIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been sent. + * The result code will be <code>Activity.RESULT_OK</code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> each sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been delivered + * to the recipient. The raw pdu of the status report is in the + * extended data ("pdu"). + * + * @throws IllegalArgumentException if messageUri is empty + * {@hide} + */ + public void sendStoredMultipartTextMessage(Uri messageUri, String scAddress, + ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents) { + if (messageUri == null) { + throw new IllegalArgumentException("Empty message URI"); + } + try { + ISms iccISms = getISmsServiceOrThrow(); + iccISms.sendStoredMultipartText( + getSubscriptionId(), ActivityThread.currentPackageName(), messageUri, + scAddress, sentIntents, deliveryIntents); + } catch (RemoteException ex) { + // ignore it + } + } + + /** + * Send a system stored MMS message + * + * This is used for sending a previously sent, but failed-to-send, message or + * for sending a text message that has been stored as a draft. + * + * @param messageUri the URI of the stored message + * @param configOverrides the carrier-specific messaging configuration values to override for + * sending the message. + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is successfully sent, or failed + * @throws IllegalArgumentException if messageUri is empty + * {@hide} + */ + public void sendStoredMultimediaMessage(Uri messageUri, Bundle configOverrides, + PendingIntent sentIntent) { + if (messageUri == null) { + throw new IllegalArgumentException("Empty message URI"); + } + try { + IMms iMms = IMms.Stub.asInterface(ServiceManager.getService("imms")); + if (iMms != null) { + iMms.sendStoredMessage( + getSubscriptionId(), ActivityThread.currentPackageName(), messageUri, + configOverrides, sentIntent); + } + } catch (RemoteException ex) { + // ignore it + } + } + + /** + * Turns on/off the flag to automatically write sent/received SMS/MMS messages into system + * + * When this flag is on, all SMS/MMS sent/received are stored by system automatically + * When this flag is off, only SMS/MMS sent by non-default SMS apps are stored by system + * automatically + * + * This flag can only be changed by default SMS apps + * + * @param enabled Whether to enable message auto persisting + * {@hide} + */ + public void setAutoPersisting(boolean enabled) { + try { + IMms iMms = IMms.Stub.asInterface(ServiceManager.getService("imms")); + if (iMms != null) { + iMms.setAutoPersisting(ActivityThread.currentPackageName(), enabled); + } + } catch (RemoteException ex) { + // ignore it + } + } + + /** + * Get the value of the flag to automatically write sent/received SMS/MMS messages into system + * + * When this flag is on, all SMS/MMS sent/received are stored by system automatically + * When this flag is off, only SMS/MMS sent by non-default SMS apps are stored by system + * automatically + * + * @return the current value of the auto persist flag + * {@hide} + */ + public boolean getAutoPersisting() { + try { + IMms iMms = IMms.Stub.asInterface(ServiceManager.getService("imms")); + if (iMms != null) { + return iMms.getAutoPersisting(); + } + } catch (RemoteException ex) { + // ignore it + } + return false; + } + + /** + * Get carrier-dependent configuration values. + * + * @return bundle key/values pairs of configuration values + */ + public Bundle getCarrierConfigValues() { + try { + IMms iMms = IMms.Stub.asInterface(ServiceManager.getService("imms")); + if (iMms != null) { + return iMms.getCarrierConfigValues(getSubscriptionId()); + } + } catch (RemoteException ex) { + // ignore it + } + return null; + } + + /** + * Filters a bundle to only contain MMS config variables. + * + * This is for use with bundles returned by {@link CarrierConfigManager} which contain MMS + * config and unrelated config. It is assumed that all MMS_CONFIG_* keys are present in the + * supplied bundle. + * + * @param config a Bundle that contains MMS config variables and possibly more. + * @return a new Bundle that only contains the MMS_CONFIG_* keys defined above. + * @hide + */ + public static Bundle getMmsConfig(BaseBundle config) { + Bundle filtered = new Bundle(); + filtered.putBoolean(MMS_CONFIG_APPEND_TRANSACTION_ID, + config.getBoolean(MMS_CONFIG_APPEND_TRANSACTION_ID)); + filtered.putBoolean(MMS_CONFIG_MMS_ENABLED, config.getBoolean(MMS_CONFIG_MMS_ENABLED)); + filtered.putBoolean(MMS_CONFIG_GROUP_MMS_ENABLED, + config.getBoolean(MMS_CONFIG_GROUP_MMS_ENABLED)); + filtered.putBoolean(MMS_CONFIG_NOTIFY_WAP_MMSC_ENABLED, + config.getBoolean(MMS_CONFIG_NOTIFY_WAP_MMSC_ENABLED)); + filtered.putBoolean(MMS_CONFIG_ALIAS_ENABLED, config.getBoolean(MMS_CONFIG_ALIAS_ENABLED)); + filtered.putBoolean(MMS_CONFIG_ALLOW_ATTACH_AUDIO, + config.getBoolean(MMS_CONFIG_ALLOW_ATTACH_AUDIO)); + filtered.putBoolean(MMS_CONFIG_MULTIPART_SMS_ENABLED, + config.getBoolean(MMS_CONFIG_MULTIPART_SMS_ENABLED)); + filtered.putBoolean(MMS_CONFIG_SMS_DELIVERY_REPORT_ENABLED, + config.getBoolean(MMS_CONFIG_SMS_DELIVERY_REPORT_ENABLED)); + filtered.putBoolean(MMS_CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION, + config.getBoolean(MMS_CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION)); + filtered.putBoolean(MMS_CONFIG_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES, + config.getBoolean(MMS_CONFIG_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES)); + filtered.putBoolean(MMS_CONFIG_MMS_READ_REPORT_ENABLED, + config.getBoolean(MMS_CONFIG_MMS_READ_REPORT_ENABLED)); + filtered.putBoolean(MMS_CONFIG_MMS_DELIVERY_REPORT_ENABLED, + config.getBoolean(MMS_CONFIG_MMS_DELIVERY_REPORT_ENABLED)); + filtered.putBoolean(MMS_CONFIG_CLOSE_CONNECTION, + config.getBoolean(MMS_CONFIG_CLOSE_CONNECTION)); + filtered.putInt(MMS_CONFIG_MAX_MESSAGE_SIZE, config.getInt(MMS_CONFIG_MAX_MESSAGE_SIZE)); + filtered.putInt(MMS_CONFIG_MAX_IMAGE_WIDTH, config.getInt(MMS_CONFIG_MAX_IMAGE_WIDTH)); + filtered.putInt(MMS_CONFIG_MAX_IMAGE_HEIGHT, config.getInt(MMS_CONFIG_MAX_IMAGE_HEIGHT)); + filtered.putInt(MMS_CONFIG_RECIPIENT_LIMIT, config.getInt(MMS_CONFIG_RECIPIENT_LIMIT)); + filtered.putInt(MMS_CONFIG_ALIAS_MIN_CHARS, config.getInt(MMS_CONFIG_ALIAS_MIN_CHARS)); + filtered.putInt(MMS_CONFIG_ALIAS_MAX_CHARS, config.getInt(MMS_CONFIG_ALIAS_MAX_CHARS)); + filtered.putInt(MMS_CONFIG_SMS_TO_MMS_TEXT_THRESHOLD, + config.getInt(MMS_CONFIG_SMS_TO_MMS_TEXT_THRESHOLD)); + filtered.putInt(MMS_CONFIG_SMS_TO_MMS_TEXT_LENGTH_THRESHOLD, + config.getInt(MMS_CONFIG_SMS_TO_MMS_TEXT_LENGTH_THRESHOLD)); + filtered.putInt(MMS_CONFIG_MESSAGE_TEXT_MAX_SIZE, + config.getInt(MMS_CONFIG_MESSAGE_TEXT_MAX_SIZE)); + filtered.putInt(MMS_CONFIG_SUBJECT_MAX_LENGTH, + config.getInt(MMS_CONFIG_SUBJECT_MAX_LENGTH)); + filtered.putInt(MMS_CONFIG_HTTP_SOCKET_TIMEOUT, + config.getInt(MMS_CONFIG_HTTP_SOCKET_TIMEOUT)); + filtered.putString(MMS_CONFIG_UA_PROF_TAG_NAME, + config.getString(MMS_CONFIG_UA_PROF_TAG_NAME)); + filtered.putString(MMS_CONFIG_USER_AGENT, config.getString(MMS_CONFIG_USER_AGENT)); + filtered.putString(MMS_CONFIG_UA_PROF_URL, config.getString(MMS_CONFIG_UA_PROF_URL)); + filtered.putString(MMS_CONFIG_HTTP_PARAMS, config.getString(MMS_CONFIG_HTTP_PARAMS)); + filtered.putString(MMS_CONFIG_EMAIL_GATEWAY_NUMBER, + config.getString(MMS_CONFIG_EMAIL_GATEWAY_NUMBER)); + filtered.putString(MMS_CONFIG_NAI_SUFFIX, config.getString(MMS_CONFIG_NAI_SUFFIX)); + filtered.putBoolean(MMS_CONFIG_SHOW_CELL_BROADCAST_APP_LINKS, + config.getBoolean(MMS_CONFIG_SHOW_CELL_BROADCAST_APP_LINKS)); + filtered.putBoolean(MMS_CONFIG_SUPPORT_HTTP_CHARSET_HEADER, + config.getBoolean(MMS_CONFIG_SUPPORT_HTTP_CHARSET_HEADER)); + return filtered; + } + +} diff --git a/telephony/java/android/telephony/SmsMessage.java b/telephony/java/android/telephony/SmsMessage.java new file mode 100644 index 000000000000..73e1f1a6ea0e --- /dev/null +++ b/telephony/java/android/telephony/SmsMessage.java @@ -0,0 +1,890 @@ +/* + * Copyright (C) 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.telephony; + +import android.os.Binder; +import android.os.Parcel; +import android.content.res.Resources; +import android.text.TextUtils; + +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails; +import com.android.internal.telephony.SmsConstants; +import com.android.internal.telephony.SmsMessageBase; +import com.android.internal.telephony.SmsMessageBase.SubmitPduBase; +import com.android.internal.telephony.Sms7BitEncodingTranslator; + +import java.lang.Math; +import java.util.ArrayList; +import java.util.Arrays; + +import static android.telephony.TelephonyManager.PHONE_TYPE_CDMA; + + +/** + * A Short Message Service message. + * @see android.provider.Telephony.Sms.Intents#getMessagesFromIntent + */ +public class SmsMessage { + private static final String LOG_TAG = "SmsMessage"; + + /** + * SMS Class enumeration. + * See TS 23.038. + * + */ + public enum MessageClass{ + UNKNOWN, CLASS_0, CLASS_1, CLASS_2, CLASS_3; + } + + /** User data text encoding code unit size */ + public static final int ENCODING_UNKNOWN = 0; + public static final int ENCODING_7BIT = 1; + public static final int ENCODING_8BIT = 2; + public static final int ENCODING_16BIT = 3; + /** + * @hide This value is not defined in global standard. Only in Korea, this is used. + */ + public static final int ENCODING_KSC5601 = 4; + + /** The maximum number of payload bytes per message */ + public static final int MAX_USER_DATA_BYTES = 140; + + /** + * The maximum number of payload bytes per message if a user data header + * is present. This assumes the header only contains the + * CONCATENATED_8_BIT_REFERENCE element. + */ + public static final int MAX_USER_DATA_BYTES_WITH_HEADER = 134; + + /** The maximum number of payload septets per message */ + public static final int MAX_USER_DATA_SEPTETS = 160; + + /** + * The maximum number of payload septets per message if a user data header + * is present. This assumes the header only contains the + * CONCATENATED_8_BIT_REFERENCE element. + */ + public static final int MAX_USER_DATA_SEPTETS_WITH_HEADER = 153; + + /** + * Indicates a 3GPP format SMS message. + * @hide pending API council approval + */ + public static final String FORMAT_3GPP = "3gpp"; + + /** + * Indicates a 3GPP2 format SMS message. + * @hide pending API council approval + */ + public static final String FORMAT_3GPP2 = "3gpp2"; + + /** Contains actual SmsMessage. Only public for debugging and for framework layer. + * + * @hide + */ + public SmsMessageBase mWrappedSmsMessage; + + /** Indicates the subId + * + * @hide + */ + private int mSubId = 0; + + /** set Subscription information + * + * @hide + */ + public void setSubId(int subId) { + mSubId = subId; + } + + /** get Subscription information + * + * @hide + */ + public int getSubId() { + return mSubId; + } + + public static class SubmitPdu { + + public byte[] encodedScAddress; // Null if not applicable. + public byte[] encodedMessage; + + @Override + public String toString() { + return "SubmitPdu: encodedScAddress = " + + Arrays.toString(encodedScAddress) + + ", encodedMessage = " + + Arrays.toString(encodedMessage); + } + + /** + * @hide + */ + protected SubmitPdu(SubmitPduBase spb) { + this.encodedMessage = spb.encodedMessage; + this.encodedScAddress = spb.encodedScAddress; + } + + } + + private SmsMessage(SmsMessageBase smb) { + mWrappedSmsMessage = smb; + } + + /** + * Create an SmsMessage from a raw PDU. Guess format based on Voice + * technology first, if it fails use other format. + * All applications which handle + * incoming SMS messages by processing the {@code SMS_RECEIVED_ACTION} broadcast + * intent <b>must</b> now pass the new {@code format} String extra from the intent + * into the new method {@code createFromPdu(byte[], String)} which takes an + * extra format parameter. This is required in order to correctly decode the PDU on + * devices that require support for both 3GPP and 3GPP2 formats at the same time, + * such as dual-mode GSM/CDMA and CDMA/LTE phones. + * @deprecated Use {@link #createFromPdu(byte[], String)} instead. + */ + @Deprecated + public static SmsMessage createFromPdu(byte[] pdu) { + SmsMessage message = null; + + // cdma(3gpp2) vs gsm(3gpp) format info was not given, + // guess from active voice phone type + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + String format = (PHONE_TYPE_CDMA == activePhone) ? + SmsConstants.FORMAT_3GPP2 : SmsConstants.FORMAT_3GPP; + message = createFromPdu(pdu, format); + + if (null == message || null == message.mWrappedSmsMessage) { + // decoding pdu failed based on activePhone type, must be other format + format = (PHONE_TYPE_CDMA == activePhone) ? + SmsConstants.FORMAT_3GPP : SmsConstants.FORMAT_3GPP2; + message = createFromPdu(pdu, format); + } + return message; + } + + /** + * Create an SmsMessage from a raw PDU with the specified message format. The + * message format is passed in the + * {@link android.provider.Telephony.Sms.Intents#SMS_RECEIVED_ACTION} as the {@code format} + * String extra, and will be either "3gpp" for GSM/UMTS/LTE messages in 3GPP format + * or "3gpp2" for CDMA/LTE messages in 3GPP2 format. + * + * @param pdu the message PDU from the + * {@link android.provider.Telephony.Sms.Intents#SMS_RECEIVED_ACTION} intent + * @param format the format extra from the + * {@link android.provider.Telephony.Sms.Intents#SMS_RECEIVED_ACTION} intent + */ + public static SmsMessage createFromPdu(byte[] pdu, String format) { + SmsMessageBase wrappedMessage; + + if (SmsConstants.FORMAT_3GPP2.equals(format)) { + wrappedMessage = com.android.internal.telephony.cdma.SmsMessage.createFromPdu(pdu); + } else if (SmsConstants.FORMAT_3GPP.equals(format)) { + wrappedMessage = com.android.internal.telephony.gsm.SmsMessage.createFromPdu(pdu); + } else { + Rlog.e(LOG_TAG, "createFromPdu(): unsupported message format " + format); + return null; + } + + if (wrappedMessage != null) { + return new SmsMessage(wrappedMessage); + } else { + Rlog.e(LOG_TAG, "createFromPdu(): wrappedMessage is null"); + return null; + } + } + + /** + * TS 27.005 3.4.1 lines[0] and lines[1] are the two lines read from the + * +CMT unsolicited response (PDU mode, of course) + * +CMT: [<alpha>],<length><CR><LF><pdu> + * + * Only public for debugging and for RIL + * + * {@hide} + */ + public static SmsMessage newFromCMT(String[] lines) { + // received SMS in 3GPP format + SmsMessageBase wrappedMessage = + com.android.internal.telephony.gsm.SmsMessage.newFromCMT(lines); + + if (wrappedMessage != null) { + return new SmsMessage(wrappedMessage); + } else { + Rlog.e(LOG_TAG, "newFromCMT(): wrappedMessage is null"); + return null; + } + } + + /** @hide */ + public static SmsMessage newFromParcel(Parcel p) { + // received SMS in 3GPP2 format + SmsMessageBase wrappedMessage = + com.android.internal.telephony.cdma.SmsMessage.newFromParcel(p); + + return new SmsMessage(wrappedMessage); + } + + /** + * Create an SmsMessage from an SMS EF record. + * + * @param index Index of SMS record. This should be index in ArrayList + * returned by SmsManager.getAllMessagesFromSim + 1. + * @param data Record data. + * @return An SmsMessage representing the record. + * + * @hide + */ + public static SmsMessage createFromEfRecord(int index, byte[] data) { + SmsMessageBase wrappedMessage; + + if (isCdmaVoice()) { + wrappedMessage = com.android.internal.telephony.cdma.SmsMessage.createFromEfRecord( + index, data); + } else { + wrappedMessage = com.android.internal.telephony.gsm.SmsMessage.createFromEfRecord( + index, data); + } + + if (wrappedMessage != null) { + return new SmsMessage(wrappedMessage); + } else { + Rlog.e(LOG_TAG, "createFromEfRecord(): wrappedMessage is null"); + return null; + } + } + + /** + * Get the TP-Layer-Length for the given SMS-SUBMIT PDU Basically, the + * length in bytes (not hex chars) less the SMSC header + * + * FIXME: This method is only used by a CTS test case that isn't run on CDMA devices. + * We should probably deprecate it and remove the obsolete test case. + */ + public static int getTPLayerLengthForPDU(String pdu) { + if (isCdmaVoice()) { + return com.android.internal.telephony.cdma.SmsMessage.getTPLayerLengthForPDU(pdu); + } else { + return com.android.internal.telephony.gsm.SmsMessage.getTPLayerLengthForPDU(pdu); + } + } + + /* + * TODO(cleanup): It would make some sense if the result of + * preprocessing a message to determine the proper encoding (i.e. + * the resulting data structure from calculateLength) could be + * passed as an argument to the actual final encoding function. + * This would better ensure that the logic behind size calculation + * actually matched the encoding. + */ + + /** + * Calculates the number of SMS's required to encode the message body and + * the number of characters remaining until the next message. + * + * @param msgBody the message to encode + * @param use7bitOnly if true, characters that are not part of the + * radio-specific 7-bit encoding are counted as single + * space chars. If false, and if the messageBody contains + * non-7-bit encodable characters, length is calculated + * using a 16-bit encoding. + * @return an int[4] with int[0] being the number of SMS's + * required, int[1] the number of code units used, and + * int[2] is the number of code units remaining until the + * next message. int[3] is an indicator of the encoding + * code unit size (see the ENCODING_* definitions in SmsConstants) + */ + public static int[] calculateLength(CharSequence msgBody, boolean use7bitOnly) { + // this function is for MO SMS + TextEncodingDetails ted = (useCdmaFormatForMoSms()) ? + com.android.internal.telephony.cdma.SmsMessage.calculateLength(msgBody, use7bitOnly, + true) : + com.android.internal.telephony.gsm.SmsMessage.calculateLength(msgBody, use7bitOnly); + int ret[] = new int[4]; + ret[0] = ted.msgCount; + ret[1] = ted.codeUnitCount; + ret[2] = ted.codeUnitsRemaining; + ret[3] = ted.codeUnitSize; + return ret; + } + + /** + * Divide a message text into several fragments, none bigger than + * the maximum SMS message text size. + * + * @param text text, must not be null. + * @return an <code>ArrayList</code> of strings that, in order, + * comprise the original msg text + * + * @hide + */ + public static ArrayList<String> fragmentText(String text) { + // This function is for MO SMS + TextEncodingDetails ted = (useCdmaFormatForMoSms()) ? + com.android.internal.telephony.cdma.SmsMessage.calculateLength(text, false, true) : + com.android.internal.telephony.gsm.SmsMessage.calculateLength(text, false); + + // TODO(cleanup): The code here could be rolled into the logic + // below cleanly if these MAX_* constants were defined more + // flexibly... + + int limit; + if (ted.codeUnitSize == SmsConstants.ENCODING_7BIT) { + int udhLength; + if (ted.languageTable != 0 && ted.languageShiftTable != 0) { + udhLength = GsmAlphabet.UDH_SEPTET_COST_TWO_SHIFT_TABLES; + } else if (ted.languageTable != 0 || ted.languageShiftTable != 0) { + udhLength = GsmAlphabet.UDH_SEPTET_COST_ONE_SHIFT_TABLE; + } else { + udhLength = 0; + } + + if (ted.msgCount > 1) { + udhLength += GsmAlphabet.UDH_SEPTET_COST_CONCATENATED_MESSAGE; + } + + if (udhLength != 0) { + udhLength += GsmAlphabet.UDH_SEPTET_COST_LENGTH; + } + + limit = SmsConstants.MAX_USER_DATA_SEPTETS - udhLength; + } else { + if (ted.msgCount > 1) { + limit = SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER; + // If EMS is not supported, break down EMS into single segment SMS + // and add page info " x/y". + // In the case of UCS2 encoding, we need 8 bytes for this, + // but we only have 6 bytes from UDH, so truncate the limit for + // each segment by 2 bytes (1 char). + // Make sure total number of segments is less than 10. + if (!hasEmsSupport() && ted.msgCount < 10) { + limit -= 2; + } + } else { + limit = SmsConstants.MAX_USER_DATA_BYTES; + } + } + + String newMsgBody = null; + Resources r = Resources.getSystem(); + if (r.getBoolean(com.android.internal.R.bool.config_sms_force_7bit_encoding)) { + newMsgBody = Sms7BitEncodingTranslator.translate(text); + } + if (TextUtils.isEmpty(newMsgBody)) { + newMsgBody = text; + } + int pos = 0; // Index in code units. + int textLen = newMsgBody.length(); + ArrayList<String> result = new ArrayList<String>(ted.msgCount); + while (pos < textLen) { + int nextPos = 0; // Counts code units. + if (ted.codeUnitSize == SmsConstants.ENCODING_7BIT) { + if (useCdmaFormatForMoSms() && ted.msgCount == 1) { + // For a singleton CDMA message, the encoding must be ASCII... + nextPos = pos + Math.min(limit, textLen - pos); + } else { + // For multi-segment messages, CDMA 7bit equals GSM 7bit encoding (EMS mode). + nextPos = GsmAlphabet.findGsmSeptetLimitIndex(newMsgBody, pos, limit, + ted.languageTable, ted.languageShiftTable); + } + } else { // Assume unicode. + nextPos = SmsMessageBase.findNextUnicodePosition(pos, limit, newMsgBody); + } + if ((nextPos <= pos) || (nextPos > textLen)) { + Rlog.e(LOG_TAG, "fragmentText failed (" + pos + " >= " + nextPos + " or " + + nextPos + " >= " + textLen + ")"); + break; + } + result.add(newMsgBody.substring(pos, nextPos)); + pos = nextPos; + } + return result; + } + + /** + * Calculates the number of SMS's required to encode the message body and + * the number of characters remaining until the next message, given the + * current encoding. + * + * @param messageBody the message to encode + * @param use7bitOnly if true, characters that are not part of the radio + * specific (GSM / CDMA) alphabet encoding are converted to as a + * single space characters. If false, a messageBody containing + * non-GSM or non-CDMA alphabet characters are encoded using + * 16-bit encoding. + * @return an int[4] with int[0] being the number of SMS's required, int[1] + * the number of code units used, and int[2] is the number of code + * units remaining until the next message. int[3] is the encoding + * type that should be used for the message. + */ + public static int[] calculateLength(String messageBody, boolean use7bitOnly) { + return calculateLength((CharSequence)messageBody, use7bitOnly); + } + + /* + * TODO(cleanup): It looks like there is now no useful reason why + * apps should generate pdus themselves using these routines, + * instead of handing the raw data to SMSDispatcher (and thereby + * have the phone process do the encoding). Moreover, CDMA now + * has shared state (in the form of the msgId system property) + * which can only be modified by the phone process, and hence + * makes the output of these routines incorrect. Since they now + * serve no purpose, they should probably just return null + * directly, and be deprecated. Going further in that direction, + * the above parsers of serialized pdu data should probably also + * be gotten rid of, hiding all but the necessarily visible + * structured data from client apps. A possible concern with + * doing this is that apps may be using these routines to generate + * pdus that are then sent elsewhere, some network server, for + * example, and that always returning null would thereby break + * otherwise useful apps. + */ + + /** + * Get an SMS-SUBMIT PDU for a destination address and a message. + * This method will not attempt to use any GSM national language 7 bit encodings. + * + * @param scAddress Service Centre address. Null means use default. + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + */ + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, String message, boolean statusReportRequested) { + SubmitPduBase spb; + + if (useCdmaFormatForMoSms()) { + spb = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(scAddress, + destinationAddress, message, statusReportRequested, null); + } else { + spb = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(scAddress, + destinationAddress, message, statusReportRequested); + } + + return new SubmitPdu(spb); + } + + /** + * Get an SMS-SUBMIT PDU for a data message to a destination address & port. + * This method will not attempt to use any GSM national language 7 bit encodings. + * + * @param scAddress Service Centre address. null == use default + * @param destinationAddress the address of the destination for the message + * @param destinationPort the port to deliver the message to at the + * destination + * @param data the data for the message + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + */ + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, short destinationPort, byte[] data, + boolean statusReportRequested) { + SubmitPduBase spb; + + if (useCdmaFormatForMoSms()) { + spb = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(scAddress, + destinationAddress, destinationPort, data, statusReportRequested); + } else { + spb = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(scAddress, + destinationAddress, destinationPort, data, statusReportRequested); + } + + return new SubmitPdu(spb); + } + + /** + * Returns the address of the SMS service center that relayed this message + * or null if there is none. + */ + public String getServiceCenterAddress() { + return mWrappedSmsMessage.getServiceCenterAddress(); + } + + /** + * Returns the originating address (sender) of this SMS message in String + * form or null if unavailable + */ + public String getOriginatingAddress() { + return mWrappedSmsMessage.getOriginatingAddress(); + } + + /** + * Returns the originating address, or email from address if this message + * was from an email gateway. Returns null if originating address + * unavailable. + */ + public String getDisplayOriginatingAddress() { + return mWrappedSmsMessage.getDisplayOriginatingAddress(); + } + + /** + * Returns the message body as a String, if it exists and is text based. + * @return message body is there is one, otherwise null + */ + public String getMessageBody() { + return mWrappedSmsMessage.getMessageBody(); + } + + /** + * Returns the class of this message. + */ + public MessageClass getMessageClass() { + switch(mWrappedSmsMessage.getMessageClass()) { + case CLASS_0: return MessageClass.CLASS_0; + case CLASS_1: return MessageClass.CLASS_1; + case CLASS_2: return MessageClass.CLASS_2; + case CLASS_3: return MessageClass.CLASS_3; + default: return MessageClass.UNKNOWN; + + } + } + + /** + * Returns the message body, or email message body if this message was from + * an email gateway. Returns null if message body unavailable. + */ + public String getDisplayMessageBody() { + return mWrappedSmsMessage.getDisplayMessageBody(); + } + + /** + * Unofficial convention of a subject line enclosed in parens empty string + * if not present + */ + public String getPseudoSubject() { + return mWrappedSmsMessage.getPseudoSubject(); + } + + /** + * Returns the service centre timestamp in currentTimeMillis() format + */ + public long getTimestampMillis() { + return mWrappedSmsMessage.getTimestampMillis(); + } + + /** + * Returns true if message is an email. + * + * @return true if this message came through an email gateway and email + * sender / subject / parsed body are available + */ + public boolean isEmail() { + return mWrappedSmsMessage.isEmail(); + } + + /** + * @return if isEmail() is true, body of the email sent through the gateway. + * null otherwise + */ + public String getEmailBody() { + return mWrappedSmsMessage.getEmailBody(); + } + + /** + * @return if isEmail() is true, email from address of email sent through + * the gateway. null otherwise + */ + public String getEmailFrom() { + return mWrappedSmsMessage.getEmailFrom(); + } + + /** + * Get protocol identifier. + */ + public int getProtocolIdentifier() { + return mWrappedSmsMessage.getProtocolIdentifier(); + } + + /** + * See TS 23.040 9.2.3.9 returns true if this is a "replace short message" + * SMS + */ + public boolean isReplace() { + return mWrappedSmsMessage.isReplace(); + } + + /** + * Returns true for CPHS MWI toggle message. + * + * @return true if this is a CPHS MWI toggle message See CPHS 4.2 section + * B.4.2 + */ + public boolean isCphsMwiMessage() { + return mWrappedSmsMessage.isCphsMwiMessage(); + } + + /** + * returns true if this message is a CPHS voicemail / message waiting + * indicator (MWI) clear message + */ + public boolean isMWIClearMessage() { + return mWrappedSmsMessage.isMWIClearMessage(); + } + + /** + * returns true if this message is a CPHS voicemail / message waiting + * indicator (MWI) set message + */ + public boolean isMWISetMessage() { + return mWrappedSmsMessage.isMWISetMessage(); + } + + /** + * returns true if this message is a "Message Waiting Indication Group: + * Discard Message" notification and should not be stored. + */ + public boolean isMwiDontStore() { + return mWrappedSmsMessage.isMwiDontStore(); + } + + /** + * returns the user data section minus the user data header if one was + * present. + */ + public byte[] getUserData() { + return mWrappedSmsMessage.getUserData(); + } + + /** + * Returns the raw PDU for the message. + * + * @return the raw PDU for the message. + */ + public byte[] getPdu() { + return mWrappedSmsMessage.getPdu(); + } + + /** + * Returns the status of the message on the SIM (read, unread, sent, unsent). + * + * @return the status of the message on the SIM. These are: + * SmsManager.STATUS_ON_SIM_FREE + * SmsManager.STATUS_ON_SIM_READ + * SmsManager.STATUS_ON_SIM_UNREAD + * SmsManager.STATUS_ON_SIM_SEND + * SmsManager.STATUS_ON_SIM_UNSENT + * @deprecated Use getStatusOnIcc instead. + */ + @Deprecated public int getStatusOnSim() { + return mWrappedSmsMessage.getStatusOnIcc(); + } + + /** + * Returns the status of the message on the ICC (read, unread, sent, unsent). + * + * @return the status of the message on the ICC. These are: + * SmsManager.STATUS_ON_ICC_FREE + * SmsManager.STATUS_ON_ICC_READ + * SmsManager.STATUS_ON_ICC_UNREAD + * SmsManager.STATUS_ON_ICC_SEND + * SmsManager.STATUS_ON_ICC_UNSENT + */ + public int getStatusOnIcc() { + return mWrappedSmsMessage.getStatusOnIcc(); + } + + /** + * Returns the record index of the message on the SIM (1-based index). + * @return the record index of the message on the SIM, or -1 if this + * SmsMessage was not created from a SIM SMS EF record. + * @deprecated Use getIndexOnIcc instead. + */ + @Deprecated public int getIndexOnSim() { + return mWrappedSmsMessage.getIndexOnIcc(); + } + + /** + * Returns the record index of the message on the ICC (1-based index). + * @return the record index of the message on the ICC, or -1 if this + * SmsMessage was not created from a ICC SMS EF record. + */ + public int getIndexOnIcc() { + return mWrappedSmsMessage.getIndexOnIcc(); + } + + /** + * GSM: + * For an SMS-STATUS-REPORT message, this returns the status field from + * the status report. This field indicates the status of a previously + * submitted SMS, if requested. See TS 23.040, 9.2.3.15 TP-Status for a + * description of values. + * CDMA: + * For not interfering with status codes from GSM, the value is + * shifted to the bits 31-16. + * The value is composed of an error class (bits 25-24) and a status code (bits 23-16). + * Possible codes are described in C.S0015-B, v2.0, 4.5.21. + * + * @return 0 indicates the previously sent message was received. + * See TS 23.040, 9.9.2.3.15 and C.S0015-B, v2.0, 4.5.21 + * for a description of other possible values. + */ + public int getStatus() { + return mWrappedSmsMessage.getStatus(); + } + + /** + * Return true iff the message is a SMS-STATUS-REPORT message. + */ + public boolean isStatusReportMessage() { + return mWrappedSmsMessage.isStatusReportMessage(); + } + + /** + * Returns true iff the <code>TP-Reply-Path</code> bit is set in + * this message. + */ + public boolean isReplyPathPresent() { + return mWrappedSmsMessage.isReplyPathPresent(); + } + + /** + * Determines whether or not to use CDMA format for MO SMS. + * If SMS over IMS is supported, then format is based on IMS SMS format, + * otherwise format is based on current phone type. + * + * @return true if Cdma format should be used for MO SMS, false otherwise. + */ + private static boolean useCdmaFormatForMoSms() { + if (!SmsManager.getDefault().isImsSmsSupported()) { + // use Voice technology to determine SMS format. + return isCdmaVoice(); + } + // IMS is registered with SMS support, check the SMS format supported + return (SmsConstants.FORMAT_3GPP2.equals(SmsManager.getDefault().getImsSmsFormat())); + } + + /** + * Determines whether or not to current phone type is cdma. + * + * @return true if current phone type is cdma, false otherwise. + */ + private static boolean isCdmaVoice() { + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + return (PHONE_TYPE_CDMA == activePhone); + } + + /** + * Decide if the carrier supports long SMS. + * {@hide} + */ + public static boolean hasEmsSupport() { + if (!isNoEmsSupportConfigListExisted()) { + return true; + } + + String simOperator; + String gid; + final long identity = Binder.clearCallingIdentity(); + try { + simOperator = TelephonyManager.getDefault().getSimOperatorNumeric(); + gid = TelephonyManager.getDefault().getGroupIdLevel1(); + } finally { + Binder.restoreCallingIdentity(identity); + } + + if (!TextUtils.isEmpty(simOperator)) { + for (NoEmsSupportConfig currentConfig : mNoEmsSupportConfigList) { + if (simOperator.startsWith(currentConfig.mOperatorNumber) && + (TextUtils.isEmpty(currentConfig.mGid1) || + (!TextUtils.isEmpty(currentConfig.mGid1) && + currentConfig.mGid1.equalsIgnoreCase(gid)))) { + return false; + } + } + } + return true; + } + + /** + * Check where to add " x/y" in each SMS segment, begin or end. + * {@hide} + */ + public static boolean shouldAppendPageNumberAsPrefix() { + if (!isNoEmsSupportConfigListExisted()) { + return false; + } + + String simOperator; + String gid; + final long identity = Binder.clearCallingIdentity(); + try { + simOperator = TelephonyManager.getDefault().getSimOperatorNumeric(); + gid = TelephonyManager.getDefault().getGroupIdLevel1(); + } finally { + Binder.restoreCallingIdentity(identity); + } + + for (NoEmsSupportConfig currentConfig : mNoEmsSupportConfigList) { + if (simOperator.startsWith(currentConfig.mOperatorNumber) && + (TextUtils.isEmpty(currentConfig.mGid1) || + (!TextUtils.isEmpty(currentConfig.mGid1) + && currentConfig.mGid1.equalsIgnoreCase(gid)))) { + return currentConfig.mIsPrefix; + } + } + return false; + } + + private static class NoEmsSupportConfig { + String mOperatorNumber; + String mGid1; + boolean mIsPrefix; + + public NoEmsSupportConfig(String[] config) { + mOperatorNumber = config[0]; + mIsPrefix = "prefix".equals(config[1]); + mGid1 = config.length > 2 ? config[2] : null; + } + + @Override + public String toString() { + return "NoEmsSupportConfig { mOperatorNumber = " + mOperatorNumber + + ", mIsPrefix = " + mIsPrefix + ", mGid1 = " + mGid1 + " }"; + } + } + + private static NoEmsSupportConfig[] mNoEmsSupportConfigList = null; + private static boolean mIsNoEmsSupportConfigListLoaded = false; + + private static boolean isNoEmsSupportConfigListExisted() { + if (!mIsNoEmsSupportConfigListLoaded) { + Resources r = Resources.getSystem(); + if (r != null) { + String[] listArray = r.getStringArray( + com.android.internal.R.array.no_ems_support_sim_operators); + if ((listArray != null) && (listArray.length > 0)) { + mNoEmsSupportConfigList = new NoEmsSupportConfig[listArray.length]; + for (int i=0; i<listArray.length; i++) { + mNoEmsSupportConfigList[i] = new NoEmsSupportConfig(listArray[i].split(";")); + } + } + mIsNoEmsSupportConfigListLoaded = true; + } + } + + if (mNoEmsSupportConfigList != null && mNoEmsSupportConfigList.length != 0) { + return true; + } + + return false; + } +} diff --git a/telephony/java/android/telephony/Telephony.java b/telephony/java/android/telephony/Telephony.java new file mode 100644 index 000000000000..943a6cade853 --- /dev/null +++ b/telephony/java/android/telephony/Telephony.java @@ -0,0 +1,2975 @@ +/* + * Copyright (C) 2006 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.provider; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.TestApi; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.database.sqlite.SqliteWrapper; +import android.net.Uri; +import android.telephony.SmsMessage; +import android.telephony.SubscriptionManager; +import android.text.TextUtils; +import android.telephony.Rlog; +import android.util.Patterns; + +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.SmsApplication; + + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The Telephony provider contains data related to phone operation, specifically SMS and MMS + * messages and access to the APN list, including the MMSC to use. + * + * <p class="note"><strong>Note:</strong> These APIs are not available on all Android-powered + * devices. If your app depends on telephony features such as for managing SMS messages, include + * a <a href="{@docRoot}guide/topics/manifest/uses-feature-element.html">{@code <uses-feature>} + * </a> element in your manifest that declares the {@code "android.hardware.telephony"} hardware + * feature. Alternatively, you can check for telephony availability at runtime using either + * {@link android.content.pm.PackageManager#hasSystemFeature + * hasSystemFeature(PackageManager.FEATURE_TELEPHONY)} or {@link + * android.telephony.TelephonyManager#getPhoneType}.</p> + * + * <h3>Creating an SMS app</h3> + * + * <p>Only the default SMS app (selected by the user in system settings) is able to write to the + * SMS Provider (the tables defined within the {@code Telephony} class) and only the default SMS + * app receives the {@link android.provider.Telephony.Sms.Intents#SMS_DELIVER_ACTION} broadcast + * when the user receives an SMS or the {@link + * android.provider.Telephony.Sms.Intents#WAP_PUSH_DELIVER_ACTION} broadcast when the user + * receives an MMS.</p> + * + * <p>Any app that wants to behave as the user's default SMS app must handle the following intents: + * <ul> + * <li>In a broadcast receiver, include an intent filter for {@link Sms.Intents#SMS_DELIVER_ACTION} + * (<code>"android.provider.Telephony.SMS_DELIVER"</code>). The broadcast receiver must also + * require the {@link android.Manifest.permission#BROADCAST_SMS} permission. + * <p>This allows your app to directly receive incoming SMS messages.</p></li> + * <li>In a broadcast receiver, include an intent filter for {@link + * Sms.Intents#WAP_PUSH_DELIVER_ACTION}} ({@code "android.provider.Telephony.WAP_PUSH_DELIVER"}) + * with the MIME type <code>"application/vnd.wap.mms-message"</code>. + * The broadcast receiver must also require the {@link + * android.Manifest.permission#BROADCAST_WAP_PUSH} permission. + * <p>This allows your app to directly receive incoming MMS messages.</p></li> + * <li>In your activity that delivers new messages, include an intent filter for + * {@link android.content.Intent#ACTION_SENDTO} (<code>"android.intent.action.SENDTO" + * </code>) with schemas, <code>sms:</code>, <code>smsto:</code>, <code>mms:</code>, and + * <code>mmsto:</code>. + * <p>This allows your app to receive intents from other apps that want to deliver a + * message.</p></li> + * <li>In a service, include an intent filter for {@link + * android.telephony.TelephonyManager#ACTION_RESPOND_VIA_MESSAGE} + * (<code>"android.intent.action.RESPOND_VIA_MESSAGE"</code>) with schemas, + * <code>sms:</code>, <code>smsto:</code>, <code>mms:</code>, and <code>mmsto:</code>. + * This service must also require the {@link + * android.Manifest.permission#SEND_RESPOND_VIA_MESSAGE} permission. + * <p>This allows users to respond to incoming phone calls with an immediate text message + * using your app.</p></li> + * </ul> + * + * <p>Other apps that are not selected as the default SMS app can only <em>read</em> the SMS + * Provider, but may also be notified when a new SMS arrives by listening for the {@link + * Sms.Intents#SMS_RECEIVED_ACTION} + * broadcast, which is a non-abortable broadcast that may be delivered to multiple apps. This + * broadcast is intended for apps that—while not selected as the default SMS app—need to + * read special incoming messages such as to perform phone number verification.</p> + * + * <p>For more information about building SMS apps, read the blog post, <a + * href="http://android-developers.blogspot.com/2013/10/getting-your-sms-apps-ready-for-kitkat.html" + * >Getting Your SMS Apps Ready for KitKat</a>.</p> + * + */ +public final class Telephony { + private static final String TAG = "Telephony"; + + /** + * Not instantiable. + * @hide + */ + private Telephony() { + } + + /** + * Base columns for tables that contain text-based SMSs. + */ + public interface TextBasedSmsColumns { + + /** Message type: all messages. */ + public static final int MESSAGE_TYPE_ALL = 0; + + /** Message type: inbox. */ + public static final int MESSAGE_TYPE_INBOX = 1; + + /** Message type: sent messages. */ + public static final int MESSAGE_TYPE_SENT = 2; + + /** Message type: drafts. */ + public static final int MESSAGE_TYPE_DRAFT = 3; + + /** Message type: outbox. */ + public static final int MESSAGE_TYPE_OUTBOX = 4; + + /** Message type: failed outgoing message. */ + public static final int MESSAGE_TYPE_FAILED = 5; + + /** Message type: queued to send later. */ + public static final int MESSAGE_TYPE_QUEUED = 6; + + /** + * The type of message. + * <P>Type: INTEGER</P> + */ + public static final String TYPE = "type"; + + /** + * The thread ID of the message. + * <P>Type: INTEGER</P> + */ + public static final String THREAD_ID = "thread_id"; + + /** + * The address of the other party. + * <P>Type: TEXT</P> + */ + public static final String ADDRESS = "address"; + + /** + * The date the message was received. + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE = "date"; + + /** + * The date the message was sent. + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE_SENT = "date_sent"; + + /** + * Has the message been read? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String READ = "read"; + + /** + * Has the message been seen by the user? The "seen" flag determines + * whether we need to show a notification. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String SEEN = "seen"; + + /** + * {@code TP-Status} value for the message, or -1 if no status has been received. + * <P>Type: INTEGER</P> + */ + public static final String STATUS = "status"; + + /** TP-Status: no status received. */ + public static final int STATUS_NONE = -1; + /** TP-Status: complete. */ + public static final int STATUS_COMPLETE = 0; + /** TP-Status: pending. */ + public static final int STATUS_PENDING = 32; + /** TP-Status: failed. */ + public static final int STATUS_FAILED = 64; + + /** + * The subject of the message, if present. + * <P>Type: TEXT</P> + */ + public static final String SUBJECT = "subject"; + + /** + * The body of the message. + * <P>Type: TEXT</P> + */ + public static final String BODY = "body"; + + /** + * The ID of the sender of the conversation, if present. + * <P>Type: INTEGER (reference to item in {@code content://contacts/people})</P> + */ + public static final String PERSON = "person"; + + /** + * The protocol identifier code. + * <P>Type: INTEGER</P> + */ + public static final String PROTOCOL = "protocol"; + + /** + * Is the {@code TP-Reply-Path} flag set? + * <P>Type: BOOLEAN</P> + */ + public static final String REPLY_PATH_PRESENT = "reply_path_present"; + + /** + * The service center (SC) through which to send the message, if present. + * <P>Type: TEXT</P> + */ + public static final String SERVICE_CENTER = "service_center"; + + /** + * Is the message locked? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String LOCKED = "locked"; + + /** + * The subscription to which the message belongs to. Its value will be + * < 0 if the sub id cannot be determined. + * <p>Type: INTEGER (long) </p> + */ + public static final String SUBSCRIPTION_ID = "sub_id"; + + /** + * The MTU size of the mobile interface to which the APN connected + * @hide + */ + public static final String MTU = "mtu"; + + /** + * Error code associated with sending or receiving this message + * <P>Type: INTEGER</P> + */ + public static final String ERROR_CODE = "error_code"; + + /** + * The identity of the sender of a sent message. It is + * usually the package name of the app which sends the message. + * <p class="note"><strong>Note:</strong> + * This column is read-only. It is set by the provider and can not be changed by apps. + * <p>Type: TEXT</p> + */ + public static final String CREATOR = "creator"; + } + + /** + * Contains all text-based SMS messages. + */ + public static final class Sms implements BaseColumns, TextBasedSmsColumns { + + /** + * Not instantiable. + * @hide + */ + private Sms() { + } + + /** + * Used to determine the currently configured default SMS package. + * @param context context of the requesting application + * @return package name for the default SMS package or null + */ + public static String getDefaultSmsPackage(Context context) { + ComponentName component = SmsApplication.getDefaultSmsApplication(context, false); + if (component != null) { + return component.getPackageName(); + } + return null; + } + + /** + * Return cursor for table query. + * @hide + */ + public static Cursor query(ContentResolver cr, String[] projection) { + return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER); + } + + /** + * Return cursor for table query. + * @hide + */ + public static Cursor query(ContentResolver cr, String[] projection, + String where, String orderBy) { + return cr.query(CONTENT_URI, projection, where, + null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); + } + + /** + * The {@code content://} style URL for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sms"); + + /** + * The default sort order for this table. + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Add an SMS to the given URI. + * + * @param resolver the content resolver to use + * @param uri the URI to add the message to + * @param address the address of the sender + * @param body the body of the message + * @param subject the pseudo-subject of the message + * @param date the timestamp for the message + * @param read true if the message has been read, false if not + * @param deliveryReport true if a delivery report was requested, false if not + * @return the URI for the new message + * @hide + */ + public static Uri addMessageToUri(ContentResolver resolver, + Uri uri, String address, String body, String subject, + Long date, boolean read, boolean deliveryReport) { + return addMessageToUri(SubscriptionManager.getDefaultSmsSubscriptionId(), + resolver, uri, address, body, subject, date, read, deliveryReport, -1L); + } + + /** + * Add an SMS to the given URI. + * + * @param resolver the content resolver to use + * @param uri the URI to add the message to + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @param read true if the message has been read, false if not + * @param deliveryReport true if a delivery report was requested, false if not + * @param subId the subscription which the message belongs to + * @return the URI for the new message + * @hide + */ + public static Uri addMessageToUri(int subId, ContentResolver resolver, + Uri uri, String address, String body, String subject, + Long date, boolean read, boolean deliveryReport) { + return addMessageToUri(subId, resolver, uri, address, body, subject, + date, read, deliveryReport, -1L); + } + + /** + * Add an SMS to the given URI with the specified thread ID. + * + * @param resolver the content resolver to use + * @param uri the URI to add the message to + * @param address the address of the sender + * @param body the body of the message + * @param subject the pseudo-subject of the message + * @param date the timestamp for the message + * @param read true if the message has been read, false if not + * @param deliveryReport true if a delivery report was requested, false if not + * @param threadId the thread_id of the message + * @return the URI for the new message + * @hide + */ + public static Uri addMessageToUri(ContentResolver resolver, + Uri uri, String address, String body, String subject, + Long date, boolean read, boolean deliveryReport, long threadId) { + return addMessageToUri(SubscriptionManager.getDefaultSmsSubscriptionId(), + resolver, uri, address, body, subject, + date, read, deliveryReport, threadId); + } + + /** + * Add an SMS to the given URI with thread_id specified. + * + * @param resolver the content resolver to use + * @param uri the URI to add the message to + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @param read true if the message has been read, false if not + * @param deliveryReport true if a delivery report was requested, false if not + * @param threadId the thread_id of the message + * @param subId the subscription which the message belongs to + * @return the URI for the new message + * @hide + */ + public static Uri addMessageToUri(int subId, ContentResolver resolver, + Uri uri, String address, String body, String subject, + Long date, boolean read, boolean deliveryReport, long threadId) { + ContentValues values = new ContentValues(8); + Rlog.v(TAG,"Telephony addMessageToUri sub id: " + subId); + + values.put(SUBSCRIPTION_ID, subId); + values.put(ADDRESS, address); + if (date != null) { + values.put(DATE, date); + } + values.put(READ, read ? Integer.valueOf(1) : Integer.valueOf(0)); + values.put(SUBJECT, subject); + values.put(BODY, body); + if (deliveryReport) { + values.put(STATUS, STATUS_PENDING); + } + if (threadId != -1L) { + values.put(THREAD_ID, threadId); + } + return resolver.insert(uri, values); + } + + /** + * Move a message to the given folder. + * + * @param context the context to use + * @param uri the message to move + * @param folder the folder to move to + * @return true if the operation succeeded + * @hide + */ + public static boolean moveMessageToFolder(Context context, + Uri uri, int folder, int error) { + if (uri == null) { + return false; + } + + boolean markAsUnread = false; + boolean markAsRead = false; + switch(folder) { + case MESSAGE_TYPE_INBOX: + case MESSAGE_TYPE_DRAFT: + break; + case MESSAGE_TYPE_OUTBOX: + case MESSAGE_TYPE_SENT: + markAsRead = true; + break; + case MESSAGE_TYPE_FAILED: + case MESSAGE_TYPE_QUEUED: + markAsUnread = true; + break; + default: + return false; + } + + ContentValues values = new ContentValues(3); + + values.put(TYPE, folder); + if (markAsUnread) { + values.put(READ, 0); + } else if (markAsRead) { + values.put(READ, 1); + } + values.put(ERROR_CODE, error); + + return 1 == SqliteWrapper.update(context, context.getContentResolver(), + uri, values, null, null); + } + + /** + * Returns true iff the folder (message type) identifies an + * outgoing message. + * @hide + */ + public static boolean isOutgoingFolder(int messageType) { + return (messageType == MESSAGE_TYPE_FAILED) + || (messageType == MESSAGE_TYPE_OUTBOX) + || (messageType == MESSAGE_TYPE_SENT) + || (messageType == MESSAGE_TYPE_QUEUED); + } + + /** + * Contains all text-based SMS messages in the SMS app inbox. + */ + public static final class Inbox implements BaseColumns, TextBasedSmsColumns { + + /** + * Not instantiable. + * @hide + */ + private Inbox() { + } + + /** + * The {@code content://} style URL for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sms/inbox"); + + /** + * The default sort order for this table. + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Add an SMS to the Draft box. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the pseudo-subject of the message + * @param date the timestamp for the message + * @param read true if the message has been read, false if not + * @return the URI for the new message + * @hide + */ + public static Uri addMessage(ContentResolver resolver, + String address, String body, String subject, Long date, + boolean read) { + return addMessageToUri(SubscriptionManager.getDefaultSmsSubscriptionId(), + resolver, CONTENT_URI, address, body, subject, date, read, false); + } + + /** + * Add an SMS to the Draft box. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @param read true if the message has been read, false if not + * @param subId the subscription which the message belongs to + * @return the URI for the new message + * @hide + */ + public static Uri addMessage(int subId, ContentResolver resolver, + String address, String body, String subject, Long date, boolean read) { + return addMessageToUri(subId, resolver, CONTENT_URI, address, body, + subject, date, read, false); + } + } + + /** + * Contains all sent text-based SMS messages in the SMS app. + */ + public static final class Sent implements BaseColumns, TextBasedSmsColumns { + + /** + * Not instantiable. + * @hide + */ + private Sent() { + } + + /** + * The {@code content://} style URL for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sms/sent"); + + /** + * The default sort order for this table. + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Add an SMS to the Draft box. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the pseudo-subject of the message + * @param date the timestamp for the message + * @return the URI for the new message + * @hide + */ + public static Uri addMessage(ContentResolver resolver, + String address, String body, String subject, Long date) { + return addMessageToUri(SubscriptionManager.getDefaultSmsSubscriptionId(), + resolver, CONTENT_URI, address, body, subject, date, true, false); + } + + /** + * Add an SMS to the Draft box. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @param subId the subscription which the message belongs to + * @return the URI for the new message + * @hide + */ + public static Uri addMessage(int subId, ContentResolver resolver, + String address, String body, String subject, Long date) { + return addMessageToUri(subId, resolver, CONTENT_URI, address, body, + subject, date, true, false); + } + } + + /** + * Contains all sent text-based SMS messages in the SMS app. + */ + public static final class Draft implements BaseColumns, TextBasedSmsColumns { + + /** + * Not instantiable. + * @hide + */ + private Draft() { + } + + /** + * The {@code content://} style URL for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sms/draft"); + + /** + * @hide + */ + public static Uri addMessage(ContentResolver resolver, + String address, String body, String subject, Long date) { + return addMessageToUri(SubscriptionManager.getDefaultSmsSubscriptionId(), + resolver, CONTENT_URI, address, body, subject, date, true, false); + } + + /** + * Add an SMS to the Draft box. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @param subId the subscription which the message belongs to + * @return the URI for the new message + * @hide + */ + public static Uri addMessage(int subId, ContentResolver resolver, + String address, String body, String subject, Long date) { + return addMessageToUri(subId, resolver, CONTENT_URI, address, body, + subject, date, true, false); + } + + /** + * The default sort order for this table. + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + } + + /** + * Contains all pending outgoing text-based SMS messages. + */ + public static final class Outbox implements BaseColumns, TextBasedSmsColumns { + + /** + * Not instantiable. + * @hide + */ + private Outbox() { + } + + /** + * The {@code content://} style URL for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sms/outbox"); + + /** + * The default sort order for this table. + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Add an SMS to the outbox. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the pseudo-subject of the message + * @param date the timestamp for the message + * @param deliveryReport whether a delivery report was requested for the message + * @return the URI for the new message + * @hide + */ + public static Uri addMessage(ContentResolver resolver, + String address, String body, String subject, Long date, + boolean deliveryReport, long threadId) { + return addMessageToUri(SubscriptionManager.getDefaultSmsSubscriptionId(), + resolver, CONTENT_URI, address, body, subject, date, + true, deliveryReport, threadId); + } + + /** + * Add an SMS to the Out box. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @param deliveryReport whether a delivery report was requested for the message + * @param subId the subscription which the message belongs to + * @return the URI for the new message + * @hide + */ + public static Uri addMessage(int subId, ContentResolver resolver, + String address, String body, String subject, Long date, + boolean deliveryReport, long threadId) { + return addMessageToUri(subId, resolver, CONTENT_URI, address, body, + subject, date, true, deliveryReport, threadId); + } + } + + /** + * Contains all sent text-based SMS messages in the SMS app. + */ + public static final class Conversations + implements BaseColumns, TextBasedSmsColumns { + + /** + * Not instantiable. + * @hide + */ + private Conversations() { + } + + /** + * The {@code content://} style URL for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sms/conversations"); + + /** + * The default sort order for this table. + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * The first 45 characters of the body of the message. + * <P>Type: TEXT</P> + */ + public static final String SNIPPET = "snippet"; + + /** + * The number of messages in the conversation. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_COUNT = "msg_count"; + } + + /** + * Contains constants for SMS related Intents that are broadcast. + */ + public static final class Intents { + + /** + * Not instantiable. + * @hide + */ + private Intents() { + } + + /** + * Set by BroadcastReceiver to indicate that the message was handled + * successfully. + */ + public static final int RESULT_SMS_HANDLED = 1; + + /** + * Set by BroadcastReceiver to indicate a generic error while + * processing the message. + */ + public static final int RESULT_SMS_GENERIC_ERROR = 2; + + /** + * Set by BroadcastReceiver to indicate insufficient memory to store + * the message. + */ + public static final int RESULT_SMS_OUT_OF_MEMORY = 3; + + /** + * Set by BroadcastReceiver to indicate that the message, while + * possibly valid, is of a format or encoding that is not + * supported. + */ + public static final int RESULT_SMS_UNSUPPORTED = 4; + + /** + * Set by BroadcastReceiver to indicate a duplicate incoming message. + */ + public static final int RESULT_SMS_DUPLICATED = 5; + + /** + * Activity action: Ask the user to change the default + * SMS application. This will show a dialog that asks the + * user whether they want to replace the current default + * SMS application with the one specified in + * {@link #EXTRA_PACKAGE_NAME}. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CHANGE_DEFAULT = + "android.provider.Telephony.ACTION_CHANGE_DEFAULT"; + + /** + * The PackageName string passed in as an + * extra for {@link #ACTION_CHANGE_DEFAULT} + * + * @see #ACTION_CHANGE_DEFAULT + */ + public static final String EXTRA_PACKAGE_NAME = "package"; + + /** + * Broadcast Action: A new text-based SMS message has been received + * by the device. This intent will only be delivered to the default + * sms app. That app is responsible for writing the message and notifying + * the user. The intent will have the following extra values:</p> + * + * <ul> + * <li><em>"pdus"</em> - An Object[] of byte[]s containing the PDUs + * that make up the message.</li> + * <li><em>"format"</em> - A String describing the format of the PDUs. It can + * be either "3gpp" or "3gpp2".</li> + * <li><em>"subscription"</em> - An optional long value of the subscription id which + * received the message.</li> + * <li><em>"slot"</em> - An optional int value of the SIM slot containing the + * subscription.</li> + * <li><em>"phone"</em> - An optional int value of the phone id associated with the + * subscription.</li> + * <li><em>"errorCode"</em> - An optional int error code associated with receiving + * the message.</li> + * </ul> + * + * <p>The extra values can be extracted using + * {@link #getMessagesFromIntent(Intent)}.</p> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> + * + * <p class="note"><strong>Note:</strong> + * The broadcast receiver that filters for this intent must declare + * {@link android.Manifest.permission#BROADCAST_SMS} as a required permission in + * the <a href="{@docRoot}guide/topics/manifest/receiver-element.html">{@code + * <receiver>}</a> tag. + * + * <p>Requires {@link android.Manifest.permission#RECEIVE_SMS} to receive.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SMS_DELIVER_ACTION = + "android.provider.Telephony.SMS_DELIVER"; + + /** + * Broadcast Action: A new text-based SMS message has been received + * by the device. This intent will be delivered to all registered + * receivers as a notification. These apps are not expected to write the + * message or notify the user. The intent will have the following extra + * values:</p> + * + * <ul> + * <li><em>"pdus"</em> - An Object[] of byte[]s containing the PDUs + * that make up the message.</li> + * </ul> + * + * <p>The extra values can be extracted using + * {@link #getMessagesFromIntent(Intent)}.</p> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> + * + * <p>Requires {@link android.Manifest.permission#RECEIVE_SMS} to receive.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SMS_RECEIVED_ACTION = + "android.provider.Telephony.SMS_RECEIVED"; + + /** + * Broadcast Action: A new data based SMS message has been received + * by the device. This intent will be delivered to all registered + * receivers as a notification. The intent will have the following extra + * values:</p> + * + * <ul> + * <li><em>"pdus"</em> - An Object[] of byte[]s containing the PDUs + * that make up the message.</li> + * </ul> + * + * <p>The extra values can be extracted using + * {@link #getMessagesFromIntent(Intent)}.</p> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> + * + * <p>Requires {@link android.Manifest.permission#RECEIVE_SMS} to receive.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String DATA_SMS_RECEIVED_ACTION = + "android.intent.action.DATA_SMS_RECEIVED"; + + /** + * Broadcast Action: A new WAP PUSH message has been received by the + * device. This intent will only be delivered to the default + * sms app. That app is responsible for writing the message and notifying + * the user. The intent will have the following extra values:</p> + * + * <ul> + * <li><em>"transactionId"</em> - (Integer) The WAP transaction ID</li> + * <li><em>"pduType"</em> - (Integer) The WAP PDU type</li> + * <li><em>"header"</em> - (byte[]) The header of the message</li> + * <li><em>"data"</em> - (byte[]) The data payload of the message</li> + * <li><em>"contentTypeParameters" </em> + * -(HashMap<String,String>) Any parameters associated with the content type + * (decoded from the WSP Content-Type header)</li> + * <li><em>"subscription"</em> - An optional long value of the subscription id which + * received the message.</li> + * <li><em>"slot"</em> - An optional int value of the SIM slot containing the + * subscription.</li> + * <li><em>"phone"</em> - An optional int value of the phone id associated with the + * subscription.</li> + * </ul> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> + * + * <p>The contentTypeParameters extra value is map of content parameters keyed by + * their names.</p> + * + * <p>If any unassigned well-known parameters are encountered, the key of the map will + * be 'unassigned/0x...', where '...' is the hex value of the unassigned parameter. If + * a parameter has No-Value the value in the map will be null.</p> + * + * <p>Requires {@link android.Manifest.permission#RECEIVE_MMS} or + * {@link android.Manifest.permission#RECEIVE_WAP_PUSH} (depending on WAP PUSH type) to + * receive.</p> + * + * <p class="note"><strong>Note:</strong> + * The broadcast receiver that filters for this intent must declare + * {@link android.Manifest.permission#BROADCAST_WAP_PUSH} as a required permission in + * the <a href="{@docRoot}guide/topics/manifest/receiver-element.html">{@code + * <receiver>}</a> tag. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String WAP_PUSH_DELIVER_ACTION = + "android.provider.Telephony.WAP_PUSH_DELIVER"; + + /** + * Broadcast Action: A new WAP PUSH message has been received by the + * device. This intent will be delivered to all registered + * receivers as a notification. These apps are not expected to write the + * message or notify the user. The intent will have the following extra + * values:</p> + * + * <ul> + * <li><em>"transactionId"</em> - (Integer) The WAP transaction ID</li> + * <li><em>"pduType"</em> - (Integer) The WAP PDU type</li> + * <li><em>"header"</em> - (byte[]) The header of the message</li> + * <li><em>"data"</em> - (byte[]) The data payload of the message</li> + * <li><em>"contentTypeParameters"</em> + * - (HashMap<String,String>) Any parameters associated with the content type + * (decoded from the WSP Content-Type header)</li> + * </ul> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> + * + * <p>The contentTypeParameters extra value is map of content parameters keyed by + * their names.</p> + * + * <p>If any unassigned well-known parameters are encountered, the key of the map will + * be 'unassigned/0x...', where '...' is the hex value of the unassigned parameter. If + * a parameter has No-Value the value in the map will be null.</p> + * + * <p>Requires {@link android.Manifest.permission#RECEIVE_MMS} or + * {@link android.Manifest.permission#RECEIVE_WAP_PUSH} (depending on WAP PUSH type) to + * receive.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String WAP_PUSH_RECEIVED_ACTION = + "android.provider.Telephony.WAP_PUSH_RECEIVED"; + + /** + * Broadcast Action: A new Cell Broadcast message has been received + * by the device. The intent will have the following extra + * values:</p> + * + * <ul> + * <li><em>"message"</em> - An SmsCbMessage object containing the broadcast message + * data. This is not an emergency alert, so ETWS and CMAS data will be null.</li> + * </ul> + * + * <p>The extra values can be extracted using + * {@link #getMessagesFromIntent(Intent)}.</p> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> + * + * <p>Requires {@link android.Manifest.permission#RECEIVE_SMS} to receive.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SMS_CB_RECEIVED_ACTION = + "android.provider.Telephony.SMS_CB_RECEIVED"; + + /** + * Action: A SMS based carrier provision intent. Used to identify default + * carrier provisioning app on the device. + * @hide + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + @TestApi + public static final String SMS_CARRIER_PROVISION_ACTION = + "android.provider.Telephony.SMS_CARRIER_PROVISION"; + + /** + * Broadcast Action: A new Emergency Broadcast message has been received + * by the device. The intent will have the following extra + * values:</p> + * + * <ul> + * <li><em>"message"</em> - An SmsCbMessage object containing the broadcast message + * data, including ETWS or CMAS warning notification info if present.</li> + * </ul> + * + * <p>The extra values can be extracted using + * {@link #getMessagesFromIntent(Intent)}.</p> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> + * + * <p>Requires {@link android.Manifest.permission#RECEIVE_EMERGENCY_BROADCAST} to + * receive.</p> + * @removed + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SMS_EMERGENCY_CB_RECEIVED_ACTION = + "android.provider.Telephony.SMS_EMERGENCY_CB_RECEIVED"; + + /** + * Broadcast Action: A new CDMA SMS has been received containing Service Category + * Program Data (updates the list of enabled broadcast channels). The intent will + * have the following extra values:</p> + * + * <ul> + * <li><em>"operations"</em> - An array of CdmaSmsCbProgramData objects containing + * the service category operations (add/delete/clear) to perform.</li> + * </ul> + * + * <p>The extra values can be extracted using + * {@link #getMessagesFromIntent(Intent)}.</p> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> + * + * <p>Requires {@link android.Manifest.permission#RECEIVE_SMS} to receive.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED_ACTION = + "android.provider.Telephony.SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED"; + + /** + * Broadcast Action: The SIM storage for SMS messages is full. If + * space is not freed, messages targeted for the SIM (class 2) may + * not be saved. + * + * <p>Requires {@link android.Manifest.permission#RECEIVE_SMS} to receive.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SIM_FULL_ACTION = + "android.provider.Telephony.SIM_FULL"; + + /** + * Broadcast Action: An incoming SMS has been rejected by the + * telephony framework. This intent is sent in lieu of any + * of the RECEIVED_ACTION intents. The intent will have the + * following extra value:</p> + * + * <ul> + * <li><em>"result"</em> - An int result code, e.g. {@link #RESULT_SMS_OUT_OF_MEMORY} + * indicating the error returned to the network.</li> + * </ul> + * + * <p>Requires {@link android.Manifest.permission#RECEIVE_SMS} to receive.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SMS_REJECTED_ACTION = + "android.provider.Telephony.SMS_REJECTED"; + + /** + * Broadcast Action: An incoming MMS has been downloaded. The intent is sent to all + * users, except for secondary users where SMS has been disabled and to managed + * profiles. + * @hide + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String MMS_DOWNLOADED_ACTION = + "android.provider.Telephony.MMS_DOWNLOADED"; + + /** + * Broadcast action: When the default SMS package changes, + * the previous default SMS package and the new default SMS + * package are sent this broadcast to notify them of the change. + * A boolean is specified in {@link #EXTRA_IS_DEFAULT_SMS_APP} to + * indicate whether the package is the new default SMS package. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_DEFAULT_SMS_PACKAGE_CHANGED = + "android.provider.action.DEFAULT_SMS_PACKAGE_CHANGED"; + + /** + * The IsDefaultSmsApp boolean passed as an + * extra for {@link #ACTION_DEFAULT_SMS_PACKAGE_CHANGED} to indicate whether the + * SMS app is becoming the default SMS app or is no longer the default. + * + * @see #ACTION_DEFAULT_SMS_PACKAGE_CHANGED + */ + public static final String EXTRA_IS_DEFAULT_SMS_APP = + "android.provider.extra.IS_DEFAULT_SMS_APP"; + + /** + * Broadcast action: When a change is made to the SmsProvider or + * MmsProvider by a process other than the default SMS application, + * this intent is broadcast to the default SMS application so it can + * re-sync or update the change. The uri that was used to call the provider + * can be retrieved from the intent with getData(). The actual affected uris + * (which would depend on the selection specified) are not included. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_EXTERNAL_PROVIDER_CHANGE = + "android.provider.action.EXTERNAL_PROVIDER_CHANGE"; + + /** + * Read the PDUs out of an {@link #SMS_RECEIVED_ACTION} or a + * {@link #DATA_SMS_RECEIVED_ACTION} intent. + * + * @param intent the intent to read from + * @return an array of SmsMessages for the PDUs + */ + public static SmsMessage[] getMessagesFromIntent(Intent intent) { + Object[] messages; + try { + messages = (Object[]) intent.getSerializableExtra("pdus"); + } + catch (ClassCastException e) { + Rlog.e(TAG, "getMessagesFromIntent: " + e); + return null; + } + + if (messages == null) { + Rlog.e(TAG, "pdus does not exist in the intent"); + return null; + } + + String format = intent.getStringExtra("format"); + int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, + SubscriptionManager.getDefaultSmsSubscriptionId()); + + Rlog.v(TAG, " getMessagesFromIntent sub_id : " + subId); + + int pduCount = messages.length; + SmsMessage[] msgs = new SmsMessage[pduCount]; + + for (int i = 0; i < pduCount; i++) { + byte[] pdu = (byte[]) messages[i]; + msgs[i] = SmsMessage.createFromPdu(pdu, format); + if (msgs[i] != null) msgs[i].setSubId(subId); + } + return msgs; + } + } + } + + /** + * Base columns for tables that contain MMSs. + */ + public interface BaseMmsColumns extends BaseColumns { + + /** Message box: all messages. */ + public static final int MESSAGE_BOX_ALL = 0; + /** Message box: inbox. */ + public static final int MESSAGE_BOX_INBOX = 1; + /** Message box: sent messages. */ + public static final int MESSAGE_BOX_SENT = 2; + /** Message box: drafts. */ + public static final int MESSAGE_BOX_DRAFTS = 3; + /** Message box: outbox. */ + public static final int MESSAGE_BOX_OUTBOX = 4; + /** Message box: failed. */ + public static final int MESSAGE_BOX_FAILED = 5; + + /** + * The thread ID of the message. + * <P>Type: INTEGER (long)</P> + */ + public static final String THREAD_ID = "thread_id"; + + /** + * The date the message was received. + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE = "date"; + + /** + * The date the message was sent. + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE_SENT = "date_sent"; + + /** + * The box which the message belongs to, e.g. {@link #MESSAGE_BOX_INBOX}. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_BOX = "msg_box"; + + /** + * Has the message been read? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String READ = "read"; + + /** + * Has the message been seen by the user? The "seen" flag determines + * whether we need to show a new message notification. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String SEEN = "seen"; + + /** + * Does the message have only a text part (can also have a subject) with + * no picture, slideshow, sound, etc. parts? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String TEXT_ONLY = "text_only"; + + /** + * The {@code Message-ID} of the message. + * <P>Type: TEXT</P> + */ + public static final String MESSAGE_ID = "m_id"; + + /** + * The subject of the message, if present. + * <P>Type: TEXT</P> + */ + public static final String SUBJECT = "sub"; + + /** + * The character set of the subject, if present. + * <P>Type: INTEGER</P> + */ + public static final String SUBJECT_CHARSET = "sub_cs"; + + /** + * The {@code Content-Type} of the message. + * <P>Type: TEXT</P> + */ + public static final String CONTENT_TYPE = "ct_t"; + + /** + * The {@code Content-Location} of the message. + * <P>Type: TEXT</P> + */ + public static final String CONTENT_LOCATION = "ct_l"; + + /** + * The expiry time of the message. + * <P>Type: INTEGER (long)</P> + */ + public static final String EXPIRY = "exp"; + + /** + * The class of the message. + * <P>Type: TEXT</P> + */ + public static final String MESSAGE_CLASS = "m_cls"; + + /** + * The type of the message defined by MMS spec. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_TYPE = "m_type"; + + /** + * The version of the specification that this message conforms to. + * <P>Type: INTEGER</P> + */ + public static final String MMS_VERSION = "v"; + + /** + * The size of the message. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_SIZE = "m_size"; + + /** + * The priority of the message. + * <P>Type: INTEGER</P> + */ + public static final String PRIORITY = "pri"; + + /** + * The {@code read-report} of the message. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String READ_REPORT = "rr"; + + /** + * Is read report allowed? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String REPORT_ALLOWED = "rpt_a"; + + /** + * The {@code response-status} of the message. + * <P>Type: INTEGER</P> + */ + public static final String RESPONSE_STATUS = "resp_st"; + + /** + * The {@code status} of the message. + * <P>Type: INTEGER</P> + */ + public static final String STATUS = "st"; + + /** + * The {@code transaction-id} of the message. + * <P>Type: TEXT</P> + */ + public static final String TRANSACTION_ID = "tr_id"; + + /** + * The {@code retrieve-status} of the message. + * <P>Type: INTEGER</P> + */ + public static final String RETRIEVE_STATUS = "retr_st"; + + /** + * The {@code retrieve-text} of the message. + * <P>Type: TEXT</P> + */ + public static final String RETRIEVE_TEXT = "retr_txt"; + + /** + * The character set of the retrieve-text. + * <P>Type: INTEGER</P> + */ + public static final String RETRIEVE_TEXT_CHARSET = "retr_txt_cs"; + + /** + * The {@code read-status} of the message. + * <P>Type: INTEGER</P> + */ + public static final String READ_STATUS = "read_status"; + + /** + * The {@code content-class} of the message. + * <P>Type: INTEGER</P> + */ + public static final String CONTENT_CLASS = "ct_cls"; + + /** + * The {@code delivery-report} of the message. + * <P>Type: INTEGER</P> + */ + public static final String DELIVERY_REPORT = "d_rpt"; + + /** + * The {@code delivery-time-token} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String DELIVERY_TIME_TOKEN = "d_tm_tok"; + + /** + * The {@code delivery-time} of the message. + * <P>Type: INTEGER</P> + */ + public static final String DELIVERY_TIME = "d_tm"; + + /** + * The {@code response-text} of the message. + * <P>Type: TEXT</P> + */ + public static final String RESPONSE_TEXT = "resp_txt"; + + /** + * The {@code sender-visibility} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String SENDER_VISIBILITY = "s_vis"; + + /** + * The {@code reply-charging} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String REPLY_CHARGING = "r_chg"; + + /** + * The {@code reply-charging-deadline-token} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String REPLY_CHARGING_DEADLINE_TOKEN = "r_chg_dl_tok"; + + /** + * The {@code reply-charging-deadline} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String REPLY_CHARGING_DEADLINE = "r_chg_dl"; + + /** + * The {@code reply-charging-id} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String REPLY_CHARGING_ID = "r_chg_id"; + + /** + * The {@code reply-charging-size} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String REPLY_CHARGING_SIZE = "r_chg_sz"; + + /** + * The {@code previously-sent-by} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String PREVIOUSLY_SENT_BY = "p_s_by"; + + /** + * The {@code previously-sent-date} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String PREVIOUSLY_SENT_DATE = "p_s_d"; + + /** + * The {@code store} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String STORE = "store"; + + /** + * The {@code mm-state} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String MM_STATE = "mm_st"; + + /** + * The {@code mm-flags-token} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String MM_FLAGS_TOKEN = "mm_flg_tok"; + + /** + * The {@code mm-flags} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String MM_FLAGS = "mm_flg"; + + /** + * The {@code store-status} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String STORE_STATUS = "store_st"; + + /** + * The {@code store-status-text} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String STORE_STATUS_TEXT = "store_st_txt"; + + /** + * The {@code stored} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String STORED = "stored"; + + /** + * The {@code totals} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String TOTALS = "totals"; + + /** + * The {@code mbox-totals} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String MBOX_TOTALS = "mb_t"; + + /** + * The {@code mbox-totals-token} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String MBOX_TOTALS_TOKEN = "mb_t_tok"; + + /** + * The {@code quotas} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String QUOTAS = "qt"; + + /** + * The {@code mbox-quotas} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String MBOX_QUOTAS = "mb_qt"; + + /** + * The {@code mbox-quotas-token} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String MBOX_QUOTAS_TOKEN = "mb_qt_tok"; + + /** + * The {@code message-count} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String MESSAGE_COUNT = "m_cnt"; + + /** + * The {@code start} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String START = "start"; + + /** + * The {@code distribution-indicator} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String DISTRIBUTION_INDICATOR = "d_ind"; + + /** + * The {@code element-descriptor} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String ELEMENT_DESCRIPTOR = "e_des"; + + /** + * The {@code limit} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String LIMIT = "limit"; + + /** + * The {@code recommended-retrieval-mode} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String RECOMMENDED_RETRIEVAL_MODE = "r_r_mod"; + + /** + * The {@code recommended-retrieval-mode-text} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String RECOMMENDED_RETRIEVAL_MODE_TEXT = "r_r_mod_txt"; + + /** + * The {@code status-text} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String STATUS_TEXT = "st_txt"; + + /** + * The {@code applic-id} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String APPLIC_ID = "apl_id"; + + /** + * The {@code reply-applic-id} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String REPLY_APPLIC_ID = "r_apl_id"; + + /** + * The {@code aux-applic-id} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String AUX_APPLIC_ID = "aux_apl_id"; + + /** + * The {@code drm-content} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String DRM_CONTENT = "drm_c"; + + /** + * The {@code adaptation-allowed} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String ADAPTATION_ALLOWED = "adp_a"; + + /** + * The {@code replace-id} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String REPLACE_ID = "repl_id"; + + /** + * The {@code cancel-id} of the message. + * <P>Type: TEXT</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String CANCEL_ID = "cl_id"; + + /** + * The {@code cancel-status} of the message. + * <P>Type: INTEGER</P> + * @deprecated this column is no longer supported. + * @hide + */ + @Deprecated + public static final String CANCEL_STATUS = "cl_st"; + + /** + * Is the message locked? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String LOCKED = "locked"; + + /** + * The subscription to which the message belongs to. Its value will be + * < 0 if the sub id cannot be determined. + * <p>Type: INTEGER (long)</p> + */ + public static final String SUBSCRIPTION_ID = "sub_id"; + + /** + * The identity of the sender of a sent message. It is + * usually the package name of the app which sends the message. + * <p class="note"><strong>Note:</strong> + * This column is read-only. It is set by the provider and can not be changed by apps. + * <p>Type: TEXT</p> + */ + public static final String CREATOR = "creator"; + } + + /** + * Columns for the "canonical_addresses" table used by MMS and SMS. + */ + public interface CanonicalAddressesColumns extends BaseColumns { + /** + * An address used in MMS or SMS. Email addresses are + * converted to lower case and are compared by string + * equality. Other addresses are compared using + * PHONE_NUMBERS_EQUAL. + * <P>Type: TEXT</P> + */ + public static final String ADDRESS = "address"; + } + + /** + * Columns for the "threads" table used by MMS and SMS. + */ + public interface ThreadsColumns extends BaseColumns { + + /** + * The date at which the thread was created. + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE = "date"; + + /** + * A string encoding of the recipient IDs of the recipients of + * the message, in numerical order and separated by spaces. + * <P>Type: TEXT</P> + */ + public static final String RECIPIENT_IDS = "recipient_ids"; + + /** + * The message count of the thread. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_COUNT = "message_count"; + + /** + * Indicates whether all messages of the thread have been read. + * <P>Type: INTEGER</P> + */ + public static final String READ = "read"; + + /** + * The snippet of the latest message in the thread. + * <P>Type: TEXT</P> + */ + public static final String SNIPPET = "snippet"; + + /** + * The charset of the snippet. + * <P>Type: INTEGER</P> + */ + public static final String SNIPPET_CHARSET = "snippet_cs"; + + /** + * Type of the thread, either {@link Threads#COMMON_THREAD} or + * {@link Threads#BROADCAST_THREAD}. + * <P>Type: INTEGER</P> + */ + public static final String TYPE = "type"; + + /** + * Indicates whether there is a transmission error in the thread. + * <P>Type: INTEGER</P> + */ + public static final String ERROR = "error"; + + /** + * Indicates whether this thread contains any attachments. + * <P>Type: INTEGER</P> + */ + public static final String HAS_ATTACHMENT = "has_attachment"; + + /** + * If the thread is archived + * <P>Type: INTEGER (boolean)</P> + */ + public static final String ARCHIVED = "archived"; + } + + /** + * Helper functions for the "threads" table used by MMS and SMS. + */ + public static final class Threads implements ThreadsColumns { + + private static final String[] ID_PROJECTION = { BaseColumns._ID }; + + /** + * Private {@code content://} style URL for this table. Used by + * {@link #getOrCreateThreadId(android.content.Context, java.util.Set)}. + */ + private static final Uri THREAD_ID_CONTENT_URI = Uri.parse( + "content://mms-sms/threadID"); + + /** + * The {@code content://} style URL for this table, by conversation. + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath( + MmsSms.CONTENT_URI, "conversations"); + + /** + * The {@code content://} style URL for this table, for obsolete threads. + */ + public static final Uri OBSOLETE_THREADS_URI = Uri.withAppendedPath( + CONTENT_URI, "obsolete"); + + /** Thread type: common thread. */ + public static final int COMMON_THREAD = 0; + + /** Thread type: broadcast thread. */ + public static final int BROADCAST_THREAD = 1; + + /** + * Not instantiable. + * @hide + */ + private Threads() { + } + + /** + * This is a single-recipient version of {@code getOrCreateThreadId}. + * It's convenient for use with SMS messages. + * @param context the context object to use. + * @param recipient the recipient to send to. + */ + public static long getOrCreateThreadId(Context context, String recipient) { + Set<String> recipients = new HashSet<String>(); + + recipients.add(recipient); + return getOrCreateThreadId(context, recipients); + } + + /** + * Given the recipients list and subject of an unsaved message, + * return its thread ID. If the message starts a new thread, + * allocate a new thread ID. Otherwise, use the appropriate + * existing thread ID. + * + * <p>Find the thread ID of the same set of recipients (in any order, + * without any additions). If one is found, return it. Otherwise, + * return a unique thread ID.</p> + */ + public static long getOrCreateThreadId( + Context context, Set<String> recipients) { + Uri.Builder uriBuilder = THREAD_ID_CONTENT_URI.buildUpon(); + + for (String recipient : recipients) { + if (Mms.isEmailAddress(recipient)) { + recipient = Mms.extractAddrSpec(recipient); + } + + uriBuilder.appendQueryParameter("recipient", recipient); + } + + Uri uri = uriBuilder.build(); + //if (DEBUG) Rlog.v(TAG, "getOrCreateThreadId uri: " + uri); + + Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(), + uri, ID_PROJECTION, null, null, null); + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + return cursor.getLong(0); + } else { + Rlog.e(TAG, "getOrCreateThreadId returned no rows!"); + } + } finally { + cursor.close(); + } + } + + Rlog.e(TAG, "getOrCreateThreadId failed with " + recipients.size() + " recipients"); + throw new IllegalArgumentException("Unable to find or allocate a thread ID."); + } + } + + /** + * Contains all MMS messages. + */ + public static final class Mms implements BaseMmsColumns { + + /** + * Not instantiable. + * @hide + */ + private Mms() { + } + + /** + * The {@code content://} URI for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://mms"); + + /** + * Content URI for getting MMS report requests. + */ + public static final Uri REPORT_REQUEST_URI = Uri.withAppendedPath( + CONTENT_URI, "report-request"); + + /** + * Content URI for getting MMS report status. + */ + public static final Uri REPORT_STATUS_URI = Uri.withAppendedPath( + CONTENT_URI, "report-status"); + + /** + * The default sort order for this table. + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Regex pattern for names and email addresses. + * <ul> + * <li><em>mailbox</em> = {@code name-addr}</li> + * <li><em>name-addr</em> = {@code [display-name] angle-addr}</li> + * <li><em>angle-addr</em> = {@code [CFWS] "<" addr-spec ">" [CFWS]}</li> + * </ul> + * @hide + */ + public static final Pattern NAME_ADDR_EMAIL_PATTERN = + Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*"); + + /** + * Helper method to query this table. + * @hide + */ + public static Cursor query( + ContentResolver cr, String[] projection) { + return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER); + } + + /** + * Helper method to query this table. + * @hide + */ + public static Cursor query( + ContentResolver cr, String[] projection, + String where, String orderBy) { + return cr.query(CONTENT_URI, projection, + where, null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); + } + + /** + * Helper method to extract email address from address string. + * @hide + */ + public static String extractAddrSpec(String address) { + Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(address); + + if (match.matches()) { + return match.group(2); + } + return address; + } + + /** + * Is the specified address an email address? + * + * @param address the input address to test + * @return true if address is an email address; false otherwise. + * @hide + */ + public static boolean isEmailAddress(String address) { + if (TextUtils.isEmpty(address)) { + return false; + } + + String s = extractAddrSpec(address); + Matcher match = Patterns.EMAIL_ADDRESS.matcher(s); + return match.matches(); + } + + /** + * Is the specified number a phone number? + * + * @param number the input number to test + * @return true if number is a phone number; false otherwise. + * @hide + */ + public static boolean isPhoneNumber(String number) { + if (TextUtils.isEmpty(number)) { + return false; + } + + Matcher match = Patterns.PHONE.matcher(number); + return match.matches(); + } + + /** + * Contains all MMS messages in the MMS app inbox. + */ + public static final class Inbox implements BaseMmsColumns { + + /** + * Not instantiable. + * @hide + */ + private Inbox() { + } + + /** + * The {@code content://} style URL for this table. + */ + public static final Uri + CONTENT_URI = Uri.parse("content://mms/inbox"); + + /** + * The default sort order for this table. + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + } + + /** + * Contains all MMS messages in the MMS app sent folder. + */ + public static final class Sent implements BaseMmsColumns { + + /** + * Not instantiable. + * @hide + */ + private Sent() { + } + + /** + * The {@code content://} style URL for this table. + */ + public static final Uri + CONTENT_URI = Uri.parse("content://mms/sent"); + + /** + * The default sort order for this table. + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + } + + /** + * Contains all MMS messages in the MMS app drafts folder. + */ + public static final class Draft implements BaseMmsColumns { + + /** + * Not instantiable. + * @hide + */ + private Draft() { + } + + /** + * The {@code content://} style URL for this table. + */ + public static final Uri + CONTENT_URI = Uri.parse("content://mms/drafts"); + + /** + * The default sort order for this table. + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + } + + /** + * Contains all MMS messages in the MMS app outbox. + */ + public static final class Outbox implements BaseMmsColumns { + + /** + * Not instantiable. + * @hide + */ + private Outbox() { + } + + /** + * The {@code content://} style URL for this table. + */ + public static final Uri + CONTENT_URI = Uri.parse("content://mms/outbox"); + + /** + * The default sort order for this table. + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + } + + /** + * Contains address information for an MMS message. + */ + public static final class Addr implements BaseColumns { + + /** + * Not instantiable. + * @hide + */ + private Addr() { + } + + /** + * The ID of MM which this address entry belongs to. + * <P>Type: INTEGER (long)</P> + */ + public static final String MSG_ID = "msg_id"; + + /** + * The ID of contact entry in Phone Book. + * <P>Type: INTEGER (long)</P> + */ + public static final String CONTACT_ID = "contact_id"; + + /** + * The address text. + * <P>Type: TEXT</P> + */ + public static final String ADDRESS = "address"; + + /** + * Type of address: must be one of {@code PduHeaders.BCC}, + * {@code PduHeaders.CC}, {@code PduHeaders.FROM}, {@code PduHeaders.TO}. + * <P>Type: INTEGER</P> + */ + public static final String TYPE = "type"; + + /** + * Character set of this entry (MMS charset value). + * <P>Type: INTEGER</P> + */ + public static final String CHARSET = "charset"; + } + + /** + * Contains message parts. + */ + public static final class Part implements BaseColumns { + + /** + * Not instantiable. + * @hide + */ + private Part() { + } + + /** + * The identifier of the message which this part belongs to. + * <P>Type: INTEGER</P> + */ + public static final String MSG_ID = "mid"; + + /** + * The order of the part. + * <P>Type: INTEGER</P> + */ + public static final String SEQ = "seq"; + + /** + * The content type of the part. + * <P>Type: TEXT</P> + */ + public static final String CONTENT_TYPE = "ct"; + + /** + * The name of the part. + * <P>Type: TEXT</P> + */ + public static final String NAME = "name"; + + /** + * The charset of the part. + * <P>Type: TEXT</P> + */ + public static final String CHARSET = "chset"; + + /** + * The file name of the part. + * <P>Type: TEXT</P> + */ + public static final String FILENAME = "fn"; + + /** + * The content disposition of the part. + * <P>Type: TEXT</P> + */ + public static final String CONTENT_DISPOSITION = "cd"; + + /** + * The content ID of the part. + * <P>Type: INTEGER</P> + */ + public static final String CONTENT_ID = "cid"; + + /** + * The content location of the part. + * <P>Type: INTEGER</P> + */ + public static final String CONTENT_LOCATION = "cl"; + + /** + * The start of content-type of the message. + * <P>Type: INTEGER</P> + */ + public static final String CT_START = "ctt_s"; + + /** + * The type of content-type of the message. + * <P>Type: TEXT</P> + */ + public static final String CT_TYPE = "ctt_t"; + + /** + * The location (on filesystem) of the binary data of the part. + * <P>Type: INTEGER</P> + */ + public static final String _DATA = "_data"; + + /** + * The message text. + * <P>Type: TEXT</P> + */ + public static final String TEXT = "text"; + } + + /** + * Message send rate table. + */ + public static final class Rate { + + /** + * Not instantiable. + * @hide + */ + private Rate() { + } + + /** + * The {@code content://} style URL for this table. + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath( + Mms.CONTENT_URI, "rate"); + + /** + * When a message was successfully sent. + * <P>Type: INTEGER (long)</P> + */ + public static final String SENT_TIME = "sent_time"; + } + + /** + * Intents class. + */ + public static final class Intents { + + /** + * Not instantiable. + * @hide + */ + private Intents() { + } + + /** + * Indicates that the contents of specified URIs were changed. + * The application which is showing or caching these contents + * should be updated. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String CONTENT_CHANGED_ACTION + = "android.intent.action.CONTENT_CHANGED"; + + /** + * An extra field which stores the URI of deleted contents. + */ + public static final String DELETED_CONTENTS = "deleted_contents"; + } + } + + /** + * Contains all MMS and SMS messages. + */ + public static final class MmsSms implements BaseColumns { + + /** + * Not instantiable. + * @hide + */ + private MmsSms() { + } + + /** + * The column to distinguish SMS and MMS messages in query results. + */ + public static final String TYPE_DISCRIMINATOR_COLUMN = + "transport_type"; + + /** + * The {@code content://} style URL for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://mms-sms/"); + + /** + * The {@code content://} style URL for this table, by conversation. + */ + public static final Uri CONTENT_CONVERSATIONS_URI = Uri.parse( + "content://mms-sms/conversations"); + + /** + * The {@code content://} style URL for this table, by phone number. + */ + public static final Uri CONTENT_FILTER_BYPHONE_URI = Uri.parse( + "content://mms-sms/messages/byphone"); + + /** + * The {@code content://} style URL for undelivered messages in this table. + */ + public static final Uri CONTENT_UNDELIVERED_URI = Uri.parse( + "content://mms-sms/undelivered"); + + /** + * The {@code content://} style URL for draft messages in this table. + */ + public static final Uri CONTENT_DRAFT_URI = Uri.parse( + "content://mms-sms/draft"); + + /** + * The {@code content://} style URL for locked messages in this table. + */ + public static final Uri CONTENT_LOCKED_URI = Uri.parse( + "content://mms-sms/locked"); + + /** + * Pass in a query parameter called "pattern" which is the text to search for. + * The sort order is fixed to be: {@code thread_id ASC, date DESC}. + */ + public static final Uri SEARCH_URI = Uri.parse( + "content://mms-sms/search"); + + // Constants for message protocol types. + + /** SMS protocol type. */ + public static final int SMS_PROTO = 0; + + /** MMS protocol type. */ + public static final int MMS_PROTO = 1; + + // Constants for error types of pending messages. + + /** Error type: no error. */ + public static final int NO_ERROR = 0; + + /** Error type: generic transient error. */ + public static final int ERR_TYPE_GENERIC = 1; + + /** Error type: SMS protocol transient error. */ + public static final int ERR_TYPE_SMS_PROTO_TRANSIENT = 2; + + /** Error type: MMS protocol transient error. */ + public static final int ERR_TYPE_MMS_PROTO_TRANSIENT = 3; + + /** Error type: transport failure. */ + public static final int ERR_TYPE_TRANSPORT_FAILURE = 4; + + /** Error type: permanent error (along with all higher error values). */ + public static final int ERR_TYPE_GENERIC_PERMANENT = 10; + + /** Error type: SMS protocol permanent error. */ + public static final int ERR_TYPE_SMS_PROTO_PERMANENT = 11; + + /** Error type: MMS protocol permanent error. */ + public static final int ERR_TYPE_MMS_PROTO_PERMANENT = 12; + + /** + * Contains pending messages info. + */ + public static final class PendingMessages implements BaseColumns { + + /** + * Not instantiable. + * @hide + */ + private PendingMessages() { + } + + public static final Uri CONTENT_URI = Uri.withAppendedPath( + MmsSms.CONTENT_URI, "pending"); + + /** + * The type of transport protocol (MMS or SMS). + * <P>Type: INTEGER</P> + */ + public static final String PROTO_TYPE = "proto_type"; + + /** + * The ID of the message to be sent or downloaded. + * <P>Type: INTEGER (long)</P> + */ + public static final String MSG_ID = "msg_id"; + + /** + * The type of the message to be sent or downloaded. + * This field is only valid for MM. For SM, its value is always set to 0. + * <P>Type: INTEGER</P> + */ + public static final String MSG_TYPE = "msg_type"; + + /** + * The type of the error code. + * <P>Type: INTEGER</P> + */ + public static final String ERROR_TYPE = "err_type"; + + /** + * The error code of sending/retrieving process. + * <P>Type: INTEGER</P> + */ + public static final String ERROR_CODE = "err_code"; + + /** + * How many times we tried to send or download the message. + * <P>Type: INTEGER</P> + */ + public static final String RETRY_INDEX = "retry_index"; + + /** + * The time to do next retry. + * <P>Type: INTEGER (long)</P> + */ + public static final String DUE_TIME = "due_time"; + + /** + * The time we last tried to send or download the message. + * <P>Type: INTEGER (long)</P> + */ + public static final String LAST_TRY = "last_try"; + + /** + * The subscription to which the message belongs to. Its value will be + * < 0 if the sub id cannot be determined. + * <p>Type: INTEGER (long) </p> + */ + public static final String SUBSCRIPTION_ID = "pending_sub_id"; + } + + /** + * Words table used by provider for full-text searches. + * @hide + */ + public static final class WordsTable { + + /** + * Not instantiable. + * @hide + */ + private WordsTable() {} + + /** + * Primary key. + * <P>Type: INTEGER (long)</P> + */ + public static final String ID = "_id"; + + /** + * Source row ID. + * <P>Type: INTEGER (long)</P> + */ + public static final String SOURCE_ROW_ID = "source_id"; + + /** + * Table ID (either 1 or 2). + * <P>Type: INTEGER</P> + */ + public static final String TABLE_ID = "table_to_use"; + + /** + * The words to index. + * <P>Type: TEXT</P> + */ + public static final String INDEXED_TEXT = "index_text"; + } + } + + /** + * Carriers class contains information about APNs, including MMSC information. + */ + public static final class Carriers implements BaseColumns { + + /** + * Not instantiable. + * @hide + */ + private Carriers() {} + + /** + * The {@code content://} style URL for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://telephony/carriers"); + + /** + * The default sort order for this table. + */ + public static final String DEFAULT_SORT_ORDER = "name ASC"; + + /** + * Entry name. + * <P>Type: TEXT</P> + */ + public static final String NAME = "name"; + + /** + * APN name. + * <P>Type: TEXT</P> + */ + public static final String APN = "apn"; + + /** + * Proxy address. + * <P>Type: TEXT</P> + */ + public static final String PROXY = "proxy"; + + /** + * Proxy port. + * <P>Type: TEXT</P> + */ + public static final String PORT = "port"; + + /** + * MMS proxy address. + * <P>Type: TEXT</P> + */ + public static final String MMSPROXY = "mmsproxy"; + + /** + * MMS proxy port. + * <P>Type: TEXT</P> + */ + public static final String MMSPORT = "mmsport"; + + /** + * Server address. + * <P>Type: TEXT</P> + */ + public static final String SERVER = "server"; + + /** + * APN username. + * <P>Type: TEXT</P> + */ + public static final String USER = "user"; + + /** + * APN password. + * <P>Type: TEXT</P> + */ + public static final String PASSWORD = "password"; + + /** + * MMSC URL. + * <P>Type: TEXT</P> + */ + public static final String MMSC = "mmsc"; + + /** + * Mobile Country Code (MCC). + * <P>Type: TEXT</P> + */ + public static final String MCC = "mcc"; + + /** + * Mobile Network Code (MNC). + * <P>Type: TEXT</P> + */ + public static final String MNC = "mnc"; + + /** + * Numeric operator ID (as String). Usually {@code MCC + MNC}. + * <P>Type: TEXT</P> + */ + public static final String NUMERIC = "numeric"; + + /** + * Authentication type. + * <P>Type: INTEGER</P> + */ + public static final String AUTH_TYPE = "authtype"; + + /** + * Comma-delimited list of APN types. + * <P>Type: TEXT</P> + */ + public static final String TYPE = "type"; + + /** + * The protocol to use to connect to this APN. + * + * One of the {@code PDP_type} values in TS 27.007 section 10.1.1. + * For example: {@code IP}, {@code IPV6}, {@code IPV4V6}, or {@code PPP}. + * <P>Type: TEXT</P> + */ + public static final String PROTOCOL = "protocol"; + + /** + * The protocol to use to connect to this APN when roaming. + * The syntax is the same as protocol. + * <P>Type: TEXT</P> + */ + public static final String ROAMING_PROTOCOL = "roaming_protocol"; + + /** + * Is this the current APN? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String CURRENT = "current"; + + /** + * Is this APN enabled? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String CARRIER_ENABLED = "carrier_enabled"; + + /** + * Radio Access Technology info. + * To check what values are allowed, refer to {@link android.telephony.ServiceState}. + * This should be spread to other technologies, + * but is currently only used for LTE (14) and eHRPD (13). + * <P>Type: INTEGER</P> + */ + public static final String BEARER = "bearer"; + + /** + * Radio Access Technology bitmask. + * To check what values can be contained, refer to {@link android.telephony.ServiceState}. + * 0 indicates all techs otherwise first bit refers to RAT/bearer 1, second bit refers to + * RAT/bearer 2 and so on. + * Bitmask for a radio tech R is (1 << (R - 1)) + * <P>Type: INTEGER</P> + * @hide + */ + public static final String BEARER_BITMASK = "bearer_bitmask"; + + /** + * MVNO type: + * {@code SPN (Service Provider Name), IMSI, GID (Group Identifier Level 1)}. + * <P>Type: TEXT</P> + */ + public static final String MVNO_TYPE = "mvno_type"; + + /** + * MVNO data. + * Use the following examples. + * <ul> + * <li>SPN: A MOBILE, BEN NL, ...</li> + * <li>IMSI: 302720x94, 2060188, ...</li> + * <li>GID: 4E, 33, ...</li> + * </ul> + * <P>Type: TEXT</P> + */ + public static final String MVNO_MATCH_DATA = "mvno_match_data"; + + /** + * The subscription to which the APN belongs to + * <p>Type: INTEGER (long) </p> + */ + public static final String SUBSCRIPTION_ID = "sub_id"; + + /** + * The profile_id to which the APN saved in modem + * <p>Type: INTEGER</p> + *@hide + */ + public static final String PROFILE_ID = "profile_id"; + + /** + * Is the apn setting to be set in modem + * <P>Type: INTEGER (boolean)</P> + *@hide + */ + public static final String MODEM_COGNITIVE = "modem_cognitive"; + + /** + * The max connections of this apn + * <p>Type: INTEGER</p> + *@hide + */ + public static final String MAX_CONNS = "max_conns"; + + /** + * The wait time for retry of the apn + * <p>Type: INTEGER</p> + *@hide + */ + public static final String WAIT_TIME = "wait_time"; + + /** + * The time to limit max connection for the apn + * <p>Type: INTEGER</p> + *@hide + */ + public static final String MAX_CONNS_TIME = "max_conns_time"; + + /** + * The MTU size of the mobile interface to which the APN connected + * <p>Type: INTEGER </p> + * @hide + */ + public static final String MTU = "mtu"; + + /** + * Is this APN added/edited/deleted by a user or carrier? + * <p>Type: INTEGER </p> + * @hide + */ + public static final String EDITED = "edited"; + + /** + * Is this APN visible to the user? + * <p>Type: INTEGER (boolean) </p> + * @hide + */ + public static final String USER_VISIBLE = "user_visible"; + + /** + * Following are possible values for the EDITED field + * @hide + */ + public static final int UNEDITED = 0; + /** + * @hide + */ + public static final int USER_EDITED = 1; + /** + * @hide + */ + public static final int USER_DELETED = 2; + /** + * DELETED_BUT_PRESENT is an intermediate value used to indicate that an entry deleted + * by the user is still present in the new APN database and therefore must remain tagged + * as user deleted rather than completely removed from the database + * @hide + */ + public static final int USER_DELETED_BUT_PRESENT_IN_XML = 3; + /** + * @hide + */ + public static final int CARRIER_EDITED = 4; + /** + * CARRIER_DELETED values are currently not used as there is no usecase. If they are used, + * delete() will have to change accordingly. Currently it is hardcoded to USER_DELETED. + * @hide + */ + public static final int CARRIER_DELETED = 5; + /** + * @hide + */ + public static final int CARRIER_DELETED_BUT_PRESENT_IN_XML = 6; + } + + /** + * Contains received SMS cell broadcast messages. + * @hide + */ + public static final class CellBroadcasts implements BaseColumns { + + /** + * Not instantiable. + * @hide + */ + private CellBroadcasts() {} + + /** + * The {@code content://} URI for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts"); + + /** + * Message geographical scope. + * <P>Type: INTEGER</P> + */ + public static final String GEOGRAPHICAL_SCOPE = "geo_scope"; + + /** + * Message serial number. + * <P>Type: INTEGER</P> + */ + public static final String SERIAL_NUMBER = "serial_number"; + + /** + * PLMN of broadcast sender. {@code SERIAL_NUMBER + PLMN + LAC + CID} uniquely identifies + * a broadcast for duplicate detection purposes. + * <P>Type: TEXT</P> + */ + public static final String PLMN = "plmn"; + + /** + * Location Area (GSM) or Service Area (UMTS) of broadcast sender. Unused for CDMA. + * Only included if Geographical Scope of message is not PLMN wide (01). + * <P>Type: INTEGER</P> + */ + public static final String LAC = "lac"; + + /** + * Cell ID of message sender (GSM/UMTS). Unused for CDMA. Only included when the + * Geographical Scope of message is cell wide (00 or 11). + * <P>Type: INTEGER</P> + */ + public static final String CID = "cid"; + + /** + * Message code. <em>OBSOLETE: merged into SERIAL_NUMBER.</em> + * <P>Type: INTEGER</P> + */ + public static final String V1_MESSAGE_CODE = "message_code"; + + /** + * Message identifier. <em>OBSOLETE: renamed to SERVICE_CATEGORY.</em> + * <P>Type: INTEGER</P> + */ + public static final String V1_MESSAGE_IDENTIFIER = "message_id"; + + /** + * Service category (GSM/UMTS: message identifier; CDMA: service category). + * <P>Type: INTEGER</P> + */ + public static final String SERVICE_CATEGORY = "service_category"; + + /** + * Message language code. + * <P>Type: TEXT</P> + */ + public static final String LANGUAGE_CODE = "language"; + + /** + * Message body. + * <P>Type: TEXT</P> + */ + public static final String MESSAGE_BODY = "body"; + + /** + * Message delivery time. + * <P>Type: INTEGER (long)</P> + */ + public static final String DELIVERY_TIME = "date"; + + /** + * Has the message been viewed? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String MESSAGE_READ = "read"; + + /** + * Message format (3GPP or 3GPP2). + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_FORMAT = "format"; + + /** + * Message priority (including emergency). + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_PRIORITY = "priority"; + + /** + * ETWS warning type (ETWS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String ETWS_WARNING_TYPE = "etws_warning_type"; + + /** + * CMAS message class (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_MESSAGE_CLASS = "cmas_message_class"; + + /** + * CMAS category (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_CATEGORY = "cmas_category"; + + /** + * CMAS response type (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_RESPONSE_TYPE = "cmas_response_type"; + + /** + * CMAS severity (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_SEVERITY = "cmas_severity"; + + /** + * CMAS urgency (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_URGENCY = "cmas_urgency"; + + /** + * CMAS certainty (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_CERTAINTY = "cmas_certainty"; + + /** The default sort order for this table. */ + public static final String DEFAULT_SORT_ORDER = DELIVERY_TIME + " DESC"; + + /** + * Query columns for instantiating {@link android.telephony.CellBroadcastMessage} objects. + */ + public static final String[] QUERY_COLUMNS = { + _ID, + GEOGRAPHICAL_SCOPE, + PLMN, + LAC, + CID, + SERIAL_NUMBER, + SERVICE_CATEGORY, + LANGUAGE_CODE, + MESSAGE_BODY, + DELIVERY_TIME, + MESSAGE_READ, + MESSAGE_FORMAT, + MESSAGE_PRIORITY, + ETWS_WARNING_TYPE, + CMAS_MESSAGE_CLASS, + CMAS_CATEGORY, + CMAS_RESPONSE_TYPE, + CMAS_SEVERITY, + CMAS_URGENCY, + CMAS_CERTAINTY + }; + } +} diff --git a/telephony/java/com/android/internal/telephony/IccUtils.java b/telephony/java/com/android/internal/telephony/IccUtils.java new file mode 100644 index 000000000000..67de87f2bf85 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/IccUtils.java @@ -0,0 +1,570 @@ +/* + * Copyright (C) 2006 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 com.android.internal.telephony.uicc; + +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.telephony.Rlog; + +import com.android.internal.telephony.GsmAlphabet; + +import java.io.UnsupportedEncodingException; + +/** + * Various methods, useful for dealing with SIM data. + */ +public class IccUtils { + static final String LOG_TAG="IccUtils"; + + /** + * Many fields in GSM SIM's are stored as nibble-swizzled BCD + * + * Assumes left-justified field that may be padded right with 0xf + * values. + * + * Stops on invalid BCD value, returning string so far + */ + public static String + bcdToString(byte[] data, int offset, int length) { + StringBuilder ret = new StringBuilder(length*2); + + for (int i = offset ; i < offset + length ; i++) { + int v; + + v = data[i] & 0xf; + if (v > 9) break; + ret.append((char)('0' + v)); + + v = (data[i] >> 4) & 0xf; + // Some PLMNs have 'f' as high nibble, ignore it + if (v == 0xf) continue; + if (v > 9) break; + ret.append((char)('0' + v)); + } + + return ret.toString(); + } + + /** + * PLMN (MCC/MNC) is encoded as per 24.008 10.5.1.3 + * Returns a concatenated string of MCC+MNC, stripping + * a trailing character for a 2-digit MNC + */ + public static String bcdPlmnToString(byte[] data, int offset) { + if (offset + 3 > data.length) { + return null; + } + byte[] trans = new byte[3]; + trans[0] = (byte) ((data[0 + offset] << 4) | ((data[0 + offset] >> 4) & 0xF)); + trans[1] = (byte) ((data[1 + offset] << 4) | (data[2 + offset] & 0xF)); + trans[2] = (byte) ((data[2 + offset] & 0xF0) | ((data[1 + offset] >> 4) & 0xF)); + String ret = bytesToHexString(trans); + + // For a 2-digit MNC we trim the trailing 'f' + if (ret.endsWith("f")) { + ret = ret.substring(0, ret.length() - 1); + } + return ret; + } + + /** + * Some fields (like ICC ID) in GSM SIMs are stored as nibble-swizzled BCH + */ + public static String + bchToString(byte[] data, int offset, int length) { + StringBuilder ret = new StringBuilder(length*2); + + for (int i = offset ; i < offset + length ; i++) { + int v; + + v = data[i] & 0xf; + ret.append("0123456789abcdef".charAt(v)); + + v = (data[i] >> 4) & 0xf; + ret.append("0123456789abcdef".charAt(v)); + } + + return ret.toString(); + } + + /** + * Decode cdma byte into String. + */ + public static String + cdmaBcdToString(byte[] data, int offset, int length) { + StringBuilder ret = new StringBuilder(length); + + int count = 0; + for (int i = offset; count < length; i++) { + int v; + v = data[i] & 0xf; + if (v > 9) v = 0; + ret.append((char)('0' + v)); + + if (++count == length) break; + + v = (data[i] >> 4) & 0xf; + if (v > 9) v = 0; + ret.append((char)('0' + v)); + ++count; + } + return ret.toString(); + } + + /** + * Decodes a GSM-style BCD byte, returning an int ranging from 0-99. + * + * In GSM land, the least significant BCD digit is stored in the most + * significant nibble. + * + * Out-of-range digits are treated as 0 for the sake of the time stamp, + * because of this: + * + * TS 23.040 section 9.2.3.11 + * "if the MS receives a non-integer value in the SCTS, it shall + * assume the digit is set to 0 but shall store the entire field + * exactly as received" + */ + public static int + gsmBcdByteToInt(byte b) { + int ret = 0; + + // treat out-of-range BCD values as 0 + if ((b & 0xf0) <= 0x90) { + ret = (b >> 4) & 0xf; + } + + if ((b & 0x0f) <= 0x09) { + ret += (b & 0xf) * 10; + } + + return ret; + } + + /** + * Decodes a CDMA style BCD byte like {@link #gsmBcdByteToInt}, but + * opposite nibble format. The least significant BCD digit + * is in the least significant nibble and the most significant + * is in the most significant nibble. + */ + public static int + cdmaBcdByteToInt(byte b) { + int ret = 0; + + // treat out-of-range BCD values as 0 + if ((b & 0xf0) <= 0x90) { + ret = ((b >> 4) & 0xf) * 10; + } + + if ((b & 0x0f) <= 0x09) { + ret += (b & 0xf); + } + + return ret; + } + + /** + * Decodes a string field that's formatted like the EF[ADN] alpha + * identifier + * + * From TS 51.011 10.5.1: + * Coding: + * this alpha tagging shall use either + * - the SMS default 7 bit coded alphabet as defined in + * TS 23.038 [12] with bit 8 set to 0. The alpha identifier + * shall be left justified. Unused bytes shall be set to 'FF'; or + * - one of the UCS2 coded options as defined in annex B. + * + * Annex B from TS 11.11 V8.13.0: + * 1) If the first octet in the alpha string is '80', then the + * remaining octets are 16 bit UCS2 characters ... + * 2) if the first octet in the alpha string is '81', then the + * second octet contains a value indicating the number of + * characters in the string, and the third octet contains an + * 8 bit number which defines bits 15 to 8 of a 16 bit + * base pointer, where bit 16 is set to zero and bits 7 to 1 + * are also set to zero. These sixteen bits constitute a + * base pointer to a "half page" in the UCS2 code space, to be + * used with some or all of the remaining octets in the string. + * The fourth and subsequent octets contain codings as follows: + * If bit 8 of the octet is set to zero, the remaining 7 bits + * of the octet contain a GSM Default Alphabet character, + * whereas if bit 8 of the octet is set to one, then the + * remaining seven bits are an offset value added to the + * 16 bit base pointer defined earlier... + * 3) If the first octet of the alpha string is set to '82', then + * the second octet contains a value indicating the number of + * characters in the string, and the third and fourth octets + * contain a 16 bit number which defines the complete 16 bit + * base pointer to a "half page" in the UCS2 code space... + */ + public static String + adnStringFieldToString(byte[] data, int offset, int length) { + if (length == 0) { + return ""; + } + if (length >= 1) { + if (data[offset] == (byte) 0x80) { + int ucslen = (length - 1) / 2; + String ret = null; + + try { + ret = new String(data, offset + 1, ucslen * 2, "utf-16be"); + } catch (UnsupportedEncodingException ex) { + Rlog.e(LOG_TAG, "implausible UnsupportedEncodingException", + ex); + } + + if (ret != null) { + // trim off trailing FFFF characters + + ucslen = ret.length(); + while (ucslen > 0 && ret.charAt(ucslen - 1) == '\uFFFF') + ucslen--; + + return ret.substring(0, ucslen); + } + } + } + + boolean isucs2 = false; + char base = '\0'; + int len = 0; + + if (length >= 3 && data[offset] == (byte) 0x81) { + len = data[offset + 1] & 0xFF; + if (len > length - 3) + len = length - 3; + + base = (char) ((data[offset + 2] & 0xFF) << 7); + offset += 3; + isucs2 = true; + } else if (length >= 4 && data[offset] == (byte) 0x82) { + len = data[offset + 1] & 0xFF; + if (len > length - 4) + len = length - 4; + + base = (char) (((data[offset + 2] & 0xFF) << 8) | + (data[offset + 3] & 0xFF)); + offset += 4; + isucs2 = true; + } + + if (isucs2) { + StringBuilder ret = new StringBuilder(); + + while (len > 0) { + // UCS2 subset case + + if (data[offset] < 0) { + ret.append((char) (base + (data[offset] & 0x7F))); + offset++; + len--; + } + + // GSM character set case + + int count = 0; + while (count < len && data[offset + count] >= 0) + count++; + + ret.append(GsmAlphabet.gsm8BitUnpackedToString(data, + offset, count)); + + offset += count; + len -= count; + } + + return ret.toString(); + } + + Resources resource = Resources.getSystem(); + String defaultCharset = ""; + try { + defaultCharset = + resource.getString(com.android.internal.R.string.gsm_alphabet_default_charset); + } catch (NotFoundException e) { + // Ignore Exception and defaultCharset is set to a empty string. + } + return GsmAlphabet.gsm8BitUnpackedToString(data, offset, length, defaultCharset.trim()); + } + + static int + hexCharToInt(char c) { + if (c >= '0' && c <= '9') return (c - '0'); + if (c >= 'A' && c <= 'F') return (c - 'A' + 10); + if (c >= 'a' && c <= 'f') return (c - 'a' + 10); + + throw new RuntimeException ("invalid hex char '" + c + "'"); + } + + /** + * Converts a hex String to a byte array. + * + * @param s A string of hexadecimal characters, must be an even number of + * chars long + * + * @return byte array representation + * + * @throws RuntimeException on invalid format + */ + public static byte[] + hexStringToBytes(String s) { + byte[] ret; + + if (s == null) return null; + + int sz = s.length(); + + ret = new byte[sz/2]; + + for (int i=0 ; i <sz ; i+=2) { + ret[i/2] = (byte) ((hexCharToInt(s.charAt(i)) << 4) + | hexCharToInt(s.charAt(i+1))); + } + + return ret; + } + + + /** + * Converts a byte array into a String of hexadecimal characters. + * + * @param bytes an array of bytes + * + * @return hex string representation of bytes array + */ + public static String + bytesToHexString(byte[] bytes) { + if (bytes == null) return null; + + StringBuilder ret = new StringBuilder(2*bytes.length); + + for (int i = 0 ; i < bytes.length ; i++) { + int b; + + b = 0x0f & (bytes[i] >> 4); + + ret.append("0123456789abcdef".charAt(b)); + + b = 0x0f & bytes[i]; + + ret.append("0123456789abcdef".charAt(b)); + } + + return ret.toString(); + } + + + /** + * Convert a TS 24.008 Section 10.5.3.5a Network Name field to a string + * "offset" points to "octet 3", the coding scheme byte + * empty string returned on decode error + */ + public static String + networkNameToString(byte[] data, int offset, int length) { + String ret; + + if ((data[offset] & 0x80) != 0x80 || length < 1) { + return ""; + } + + switch ((data[offset] >>> 4) & 0x7) { + case 0: + // SMS character set + int countSeptets; + int unusedBits = data[offset] & 7; + countSeptets = (((length - 1) * 8) - unusedBits) / 7 ; + ret = GsmAlphabet.gsm7BitPackedToString(data, offset + 1, countSeptets); + break; + case 1: + // UCS2 + try { + ret = new String(data, + offset + 1, length - 1, "utf-16"); + } catch (UnsupportedEncodingException ex) { + ret = ""; + Rlog.e(LOG_TAG,"implausible UnsupportedEncodingException", ex); + } + break; + + // unsupported encoding + default: + ret = ""; + break; + } + + // "Add CI" + // "The MS should add the letters for the Country's Initials and + // a separator (e.g. a space) to the text string" + + if ((data[offset] & 0x40) != 0) { + // FIXME(mkf) add country initials here + + } + + return ret; + } + + /** + * Convert a TS 131.102 image instance of code scheme '11' into Bitmap + * @param data The raw data + * @param length The length of image body + * @return The bitmap + */ + public static Bitmap parseToBnW(byte[] data, int length){ + int valueIndex = 0; + int width = data[valueIndex++] & 0xFF; + int height = data[valueIndex++] & 0xFF; + int numOfPixels = width*height; + + int[] pixels = new int[numOfPixels]; + + int pixelIndex = 0; + int bitIndex = 7; + byte currentByte = 0x00; + while (pixelIndex < numOfPixels) { + // reassign data and index for every byte (8 bits). + if (pixelIndex % 8 == 0) { + currentByte = data[valueIndex++]; + bitIndex = 7; + } + pixels[pixelIndex++] = bitToRGB((currentByte >> bitIndex-- ) & 0x01); + } + + if (pixelIndex != numOfPixels) { + Rlog.e(LOG_TAG, "parse end and size error"); + } + return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888); + } + + private static int bitToRGB(int bit){ + if(bit == 1){ + return Color.WHITE; + } else { + return Color.BLACK; + } + } + + /** + * a TS 131.102 image instance of code scheme '11' into color Bitmap + * + * @param data The raw data + * @param length the length of image body + * @param transparency with or without transparency + * @return The color bitmap + */ + public static Bitmap parseToRGB(byte[] data, int length, + boolean transparency) { + int valueIndex = 0; + int width = data[valueIndex++] & 0xFF; + int height = data[valueIndex++] & 0xFF; + int bits = data[valueIndex++] & 0xFF; + int colorNumber = data[valueIndex++] & 0xFF; + int clutOffset = ((data[valueIndex++] & 0xFF) << 8) + | (data[valueIndex++] & 0xFF); + + int[] colorIndexArray = getCLUT(data, clutOffset, colorNumber); + if (true == transparency) { + colorIndexArray[colorNumber - 1] = Color.TRANSPARENT; + } + + int[] resultArray = null; + if (0 == (8 % bits)) { + resultArray = mapTo2OrderBitColor(data, valueIndex, + (width * height), colorIndexArray, bits); + } else { + resultArray = mapToNon2OrderBitColor(data, valueIndex, + (width * height), colorIndexArray, bits); + } + + return Bitmap.createBitmap(resultArray, width, height, + Bitmap.Config.RGB_565); + } + + private static int[] mapTo2OrderBitColor(byte[] data, int valueIndex, + int length, int[] colorArray, int bits) { + if (0 != (8 % bits)) { + Rlog.e(LOG_TAG, "not event number of color"); + return mapToNon2OrderBitColor(data, valueIndex, length, colorArray, + bits); + } + + int mask = 0x01; + switch (bits) { + case 1: + mask = 0x01; + break; + case 2: + mask = 0x03; + break; + case 4: + mask = 0x0F; + break; + case 8: + mask = 0xFF; + break; + } + + int[] resultArray = new int[length]; + int resultIndex = 0; + int run = 8 / bits; + while (resultIndex < length) { + byte tempByte = data[valueIndex++]; + for (int runIndex = 0; runIndex < run; ++runIndex) { + int offset = run - runIndex - 1; + resultArray[resultIndex++] = colorArray[(tempByte >> (offset * bits)) + & mask]; + } + } + return resultArray; + } + + private static int[] mapToNon2OrderBitColor(byte[] data, int valueIndex, + int length, int[] colorArray, int bits) { + if (0 == (8 % bits)) { + Rlog.e(LOG_TAG, "not odd number of color"); + return mapTo2OrderBitColor(data, valueIndex, length, colorArray, + bits); + } + + int[] resultArray = new int[length]; + // TODO fix me: + return resultArray; + } + + private static int[] getCLUT(byte[] rawData, int offset, int number) { + if (null == rawData) { + return null; + } + + int[] result = new int[number]; + int endIndex = offset + (number * 3); // 1 color use 3 bytes + int valueIndex = offset; + int colorIndex = 0; + int alpha = 0xff << 24; + do { + result[colorIndex++] = alpha + | ((rawData[valueIndex++] & 0xFF) << 16) + | ((rawData[valueIndex++] & 0xFF) << 8) + | ((rawData[valueIndex++] & 0xFF)); + } while (valueIndex < endIndex); + return result; + } +} diff --git a/telephony/java/com/android/internal/telephony/Sms7BitEncodingTranslator.java b/telephony/java/com/android/internal/telephony/Sms7BitEncodingTranslator.java new file mode 100644 index 000000000000..439eaeac8de1 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/Sms7BitEncodingTranslator.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2014 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 com.android.internal.telephony; + +import android.telephony.Rlog; +import android.os.Build; +import android.util.SparseIntArray; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.telephony.SmsManager; +import android.telephony.TelephonyManager; + +import com.android.internal.util.XmlUtils; +import com.android.internal.telephony.cdma.sms.UserData; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +public class Sms7BitEncodingTranslator { + private static final String TAG = "Sms7BitEncodingTranslator"; + private static final boolean DBG = Build.IS_DEBUGGABLE ; + private static boolean mIs7BitTranslationTableLoaded = false; + private static SparseIntArray mTranslationTable = null; + private static SparseIntArray mTranslationTableCommon = null; + private static SparseIntArray mTranslationTableGSM = null; + private static SparseIntArray mTranslationTableCDMA = null; + + // Parser variables + private static final String XML_START_TAG = "SmsEnforce7BitTranslationTable"; + private static final String XML_TRANSLATION_TYPE_TAG = "TranslationType"; + private static final String XML_CHARACTOR_TAG = "Character"; + private static final String XML_FROM_TAG = "from"; + private static final String XML_TO_TAG = "to"; + + /** + * Translates each message character that is not supported by GSM 7bit + * alphabet into a supported one + * + * @param message + * message to be translated + * @param throwsException + * if true and some error occurs during translation, an exception + * is thrown; otherwise a null String is returned + * @return translated message or null if some error occur + */ + public static String translate(CharSequence message) { + if (message == null) { + Rlog.w(TAG, "Null message can not be translated"); + return null; + } + + int size = message.length(); + if (size <= 0) { + return ""; + } + + if (!mIs7BitTranslationTableLoaded) { + mTranslationTableCommon = new SparseIntArray(); + mTranslationTableGSM = new SparseIntArray(); + mTranslationTableCDMA = new SparseIntArray(); + load7BitTranslationTableFromXml(); + mIs7BitTranslationTableLoaded = true; + } + + if ((mTranslationTableCommon != null && mTranslationTableCommon.size() > 0) || + (mTranslationTableGSM != null && mTranslationTableGSM.size() > 0) || + (mTranslationTableCDMA != null && mTranslationTableCDMA.size() > 0)) { + char[] output = new char[size]; + boolean isCdmaFormat = useCdmaFormatForMoSms(); + for (int i = 0; i < size; i++) { + output[i] = translateIfNeeded(message.charAt(i), isCdmaFormat); + } + + return String.valueOf(output); + } + + return null; + } + + /** + * Translates a single character into its corresponding acceptable one, if + * needed, based on GSM 7-bit alphabet + * + * @param c + * character to be translated + * @return original character, if it's present on GSM 7-bit alphabet; a + * corresponding character, based on the translation table or white + * space, if no mapping is found in the translation table for such + * character + */ + private static char translateIfNeeded(char c, boolean isCdmaFormat) { + if (noTranslationNeeded(c, isCdmaFormat)) { + if (DBG) { + Rlog.v(TAG, "No translation needed for " + Integer.toHexString(c)); + } + return c; + } + + /* + * Trying to translate unicode to Gsm 7-bit alphabet; If c is not + * present on translation table, c does not belong to Unicode Latin-1 + * (Basic + Supplement), so we don't know how to translate it to a Gsm + * 7-bit character! We replace c for an empty space and advises the user + * about it. + */ + int translation = -1; + + if (mTranslationTableCommon != null) { + translation = mTranslationTableCommon.get(c, -1); + } + + if (translation == -1) { + if (isCdmaFormat) { + if (mTranslationTableCDMA != null) { + translation = mTranslationTableCDMA.get(c, -1); + } + } else { + if (mTranslationTableGSM != null) { + translation = mTranslationTableGSM.get(c, -1); + } + } + } + + if (translation != -1) { + if (DBG) { + Rlog.v(TAG, Integer.toHexString(c) + " (" + c + ")" + " translated to " + + Integer.toHexString(translation) + " (" + (char) translation + ")"); + } + return (char) translation; + } else { + if (DBG) { + Rlog.w(TAG, "No translation found for " + Integer.toHexString(c) + + "! Replacing for empty space"); + } + return ' '; + } + } + + private static boolean noTranslationNeeded(char c, boolean isCdmaFormat) { + if (isCdmaFormat) { + return GsmAlphabet.isGsmSeptets(c) && UserData.charToAscii.get(c, -1) != -1; + } + else { + return GsmAlphabet.isGsmSeptets(c); + } + } + + private static boolean useCdmaFormatForMoSms() { + if (!SmsManager.getDefault().isImsSmsSupported()) { + // use Voice technology to determine SMS format. + return TelephonyManager.getDefault().getCurrentPhoneType() + == PhoneConstants.PHONE_TYPE_CDMA; + } + // IMS is registered with SMS support, check the SMS format supported + return (SmsConstants.FORMAT_3GPP2.equals(SmsManager.getDefault().getImsSmsFormat())); + } + + /** + * Load the whole translation table file from the framework resource + * encoded in XML. + */ + private static void load7BitTranslationTableFromXml() { + XmlResourceParser parser = null; + Resources r = Resources.getSystem(); + + if (parser == null) { + if (DBG) Rlog.d(TAG, "load7BitTranslationTableFromXml: open normal file"); + parser = r.getXml(com.android.internal.R.xml.sms_7bit_translation_table); + } + + try { + XmlUtils.beginDocument(parser, XML_START_TAG); + while (true) { + XmlUtils.nextElement(parser); + String tag = parser.getName(); + if (DBG) { + Rlog.d(TAG, "tag: " + tag); + } + if (XML_TRANSLATION_TYPE_TAG.equals(tag)) { + String type = parser.getAttributeValue(null, "Type"); + if (DBG) { + Rlog.d(TAG, "type: " + type); + } + if (type.equals("common")) { + mTranslationTable = mTranslationTableCommon; + } else if (type.equals("gsm")) { + mTranslationTable = mTranslationTableGSM; + } else if (type.equals("cdma")) { + mTranslationTable = mTranslationTableCDMA; + } else { + Rlog.e(TAG, "Error Parsing 7BitTranslationTable: found incorrect type" + type); + } + } else if (XML_CHARACTOR_TAG.equals(tag) && mTranslationTable != null) { + int from = parser.getAttributeUnsignedIntValue(null, + XML_FROM_TAG, -1); + int to = parser.getAttributeUnsignedIntValue(null, + XML_TO_TAG, -1); + if ((from != -1) && (to != -1)) { + if (DBG) { + Rlog.d(TAG, "Loading mapping " + Integer.toHexString(from) + .toUpperCase() + " -> " + Integer.toHexString(to) + .toUpperCase()); + } + mTranslationTable.put (from, to); + } else { + Rlog.d(TAG, "Invalid translation table file format"); + } + } else { + break; + } + } + if (DBG) Rlog.d(TAG, "load7BitTranslationTableFromXml: parsing successful, file loaded"); + } catch (Exception e) { + Rlog.e(TAG, "Got exception while loading 7BitTranslationTable file.", e); + } finally { + if (parser instanceof XmlResourceParser) { + ((XmlResourceParser)parser).close(); + } + } + } +} diff --git a/telephony/java/com/android/internal/telephony/SmsAddress.java b/telephony/java/com/android/internal/telephony/SmsAddress.java new file mode 100644 index 000000000000..b3892cb0b342 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/SmsAddress.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 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 com.android.internal.telephony; + +public abstract class SmsAddress { + // From TS 23.040 9.1.2.5 and TS 24.008 table 10.5.118 + // and C.S0005-D table 2.7.1.3.2.4-2 + public static final int TON_UNKNOWN = 0; + public static final int TON_INTERNATIONAL = 1; + public static final int TON_NATIONAL = 2; + public static final int TON_NETWORK = 3; + public static final int TON_SUBSCRIBER = 4; + public static final int TON_ALPHANUMERIC = 5; + public static final int TON_ABBREVIATED = 6; + + public int ton; + public String address; + public byte[] origBytes; + + /** + * Returns the address of the SMS message in String form or null if unavailable + */ + public String getAddressString() { + return address; + } + + /** + * Returns true if this is an alphanumeric address + */ + public boolean isAlphanumeric() { + return ton == TON_ALPHANUMERIC; + } + + /** + * Returns true if this is a network address + */ + public boolean isNetworkSpecific() { + return ton == TON_NETWORK; + } + + public boolean couldBeEmailGateway() { + // Some carriers seems to send email gateway messages in this form: + // from: an UNKNOWN TON, 3 or 4 digits long, beginning with a 5 + // PID: 0x00, Data coding scheme 0x03 + // So we just attempt to treat any message from an address length <= 4 + // as an email gateway + + return address.length() <= 4; + } + +} diff --git a/telephony/java/com/android/internal/telephony/SmsApplication.java b/telephony/java/com/android/internal/telephony/SmsApplication.java new file mode 100644 index 000000000000..bf90350effb3 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/SmsApplication.java @@ -0,0 +1,924 @@ +/* + * Copyright (C) 2013 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 com.android.internal.telephony; + +import android.Manifest.permission; +import android.app.AppOpsManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.net.Uri; +import android.os.Binder; +import android.os.Debug; +import android.os.Process; +import android.os.UserHandle; +import android.provider.Settings; +import android.provider.Telephony; +import android.provider.Telephony.Sms.Intents; +import android.telephony.Rlog; +import android.telephony.SmsManager; +import android.telephony.TelephonyManager; +import android.util.Log; + +import com.android.internal.content.PackageMonitor; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.MetricsProto.MetricsEvent; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; + +/** + * Class for managing the primary application that we will deliver SMS/MMS messages to + * + * {@hide} + */ +public final class SmsApplication { + static final String LOG_TAG = "SmsApplication"; + private static final String PHONE_PACKAGE_NAME = "com.android.phone"; + private static final String BLUETOOTH_PACKAGE_NAME = "com.android.bluetooth"; + private static final String MMS_SERVICE_PACKAGE_NAME = "com.android.mms.service"; + private static final String TELEPHONY_PROVIDER_PACKAGE_NAME = "com.android.providers.telephony"; + + private static final String SCHEME_SMS = "sms"; + private static final String SCHEME_SMSTO = "smsto"; + private static final String SCHEME_MMS = "mms"; + private static final String SCHEME_MMSTO = "mmsto"; + private static final boolean DEBUG_MULTIUSER = false; + + private static SmsPackageMonitor sSmsPackageMonitor = null; + + public static class SmsApplicationData { + /** + * Name of this SMS app for display. + */ + private String mApplicationName; + + /** + * Package name for this SMS app. + */ + public String mPackageName; + + /** + * The class name of the SMS_DELIVER_ACTION receiver in this app. + */ + public String mSmsReceiverClass; + + /** + * The class name of the WAP_PUSH_DELIVER_ACTION receiver in this app. + */ + public String mMmsReceiverClass; + + /** + * The class name of the ACTION_RESPOND_VIA_MESSAGE intent in this app. + */ + public String mRespondViaMessageClass; + + /** + * The class name of the ACTION_SENDTO intent in this app. + */ + public String mSendToClass; + + /** + * The class name of the ACTION_DEFAULT_SMS_PACKAGE_CHANGED receiver in this app. + */ + public String mSmsAppChangedReceiverClass; + + /** + * The class name of the ACTION_EXTERNAL_PROVIDER_CHANGE receiver in this app. + */ + public String mProviderChangedReceiverClass; + + /** + * The user-id for this application + */ + public int mUid; + + /** + * Returns true if this SmsApplicationData is complete (all intents handled). + * @return + */ + public boolean isComplete() { + return (mSmsReceiverClass != null && mMmsReceiverClass != null + && mRespondViaMessageClass != null && mSendToClass != null); + } + + public SmsApplicationData(String packageName, int uid) { + mPackageName = packageName; + mUid = uid; + } + + public String getApplicationName(Context context) { + if (mApplicationName == null) { + PackageManager pm = context.getPackageManager(); + ApplicationInfo appInfo; + try { + appInfo = pm.getApplicationInfoAsUser(mPackageName, 0, + UserHandle.getUserId(mUid)); + } catch (NameNotFoundException e) { + return null; + } + if (appInfo != null) { + CharSequence label = pm.getApplicationLabel(appInfo); + mApplicationName = (label == null) ? null : label.toString(); + } + } + return mApplicationName; + } + + @Override + public String toString() { + return " mPackageName: " + mPackageName + + " mSmsReceiverClass: " + mSmsReceiverClass + + " mMmsReceiverClass: " + mMmsReceiverClass + + " mRespondViaMessageClass: " + mRespondViaMessageClass + + " mSendToClass: " + mSendToClass + + " mSmsAppChangedClass: " + mSmsAppChangedReceiverClass + + " mProviderChangedReceiverClass: " + mProviderChangedReceiverClass + + " mUid: " + mUid; + } + } + + /** + * Returns the userId of the Context object, if called from a system app, + * otherwise it returns the caller's userId + * @param context The context object passed in by the caller. + * @return + */ + private static int getIncomingUserId(Context context) { + int contextUserId = context.getUserId(); + final int callingUid = Binder.getCallingUid(); + if (DEBUG_MULTIUSER) { + Log.i(LOG_TAG, "getIncomingUserHandle caller=" + callingUid + ", myuid=" + + android.os.Process.myUid() + "\n\t" + Debug.getCallers(4)); + } + if (UserHandle.getAppId(callingUid) + < android.os.Process.FIRST_APPLICATION_UID) { + return contextUserId; + } else { + return UserHandle.getUserId(callingUid); + } + } + + /** + * Returns the list of available SMS apps defined as apps that are registered for both the + * SMS_RECEIVED_ACTION (SMS) and WAP_PUSH_RECEIVED_ACTION (MMS) broadcasts (and their broadcast + * receivers are enabled) + * + * Requirements to be an SMS application: + * Implement SMS_DELIVER_ACTION broadcast receiver. + * Require BROADCAST_SMS permission. + * + * Implement WAP_PUSH_DELIVER_ACTION broadcast receiver. + * Require BROADCAST_WAP_PUSH permission. + * + * Implement RESPOND_VIA_MESSAGE intent. + * Support smsto Uri scheme. + * Require SEND_RESPOND_VIA_MESSAGE permission. + * + * Implement ACTION_SENDTO intent. + * Support smsto Uri scheme. + */ + public static Collection<SmsApplicationData> getApplicationCollection(Context context) { + int userId = getIncomingUserId(context); + final long token = Binder.clearCallingIdentity(); + try { + return getApplicationCollectionInternal(context, userId); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private static Collection<SmsApplicationData> getApplicationCollectionInternal( + Context context, int userId) { + PackageManager packageManager = context.getPackageManager(); + + // Get the list of apps registered for SMS + Intent intent = new Intent(Intents.SMS_DELIVER_ACTION); + List<ResolveInfo> smsReceivers = packageManager.queryBroadcastReceiversAsUser(intent, 0, + userId); + + HashMap<String, SmsApplicationData> receivers = new HashMap<String, SmsApplicationData>(); + + // Add one entry to the map for every sms receiver (ignoring duplicate sms receivers) + for (ResolveInfo resolveInfo : smsReceivers) { + final ActivityInfo activityInfo = resolveInfo.activityInfo; + if (activityInfo == null) { + continue; + } + if (!permission.BROADCAST_SMS.equals(activityInfo.permission)) { + continue; + } + final String packageName = activityInfo.packageName; + if (!receivers.containsKey(packageName)) { + final SmsApplicationData smsApplicationData = new SmsApplicationData(packageName, + activityInfo.applicationInfo.uid); + smsApplicationData.mSmsReceiverClass = activityInfo.name; + receivers.put(packageName, smsApplicationData); + } + } + + // Update any existing entries with mms receiver class + intent = new Intent(Intents.WAP_PUSH_DELIVER_ACTION); + intent.setDataAndType(null, "application/vnd.wap.mms-message"); + List<ResolveInfo> mmsReceivers = packageManager.queryBroadcastReceiversAsUser(intent, 0, + userId); + for (ResolveInfo resolveInfo : mmsReceivers) { + final ActivityInfo activityInfo = resolveInfo.activityInfo; + if (activityInfo == null) { + continue; + } + if (!permission.BROADCAST_WAP_PUSH.equals(activityInfo.permission)) { + continue; + } + final String packageName = activityInfo.packageName; + final SmsApplicationData smsApplicationData = receivers.get(packageName); + if (smsApplicationData != null) { + smsApplicationData.mMmsReceiverClass = activityInfo.name; + } + } + + // Update any existing entries with respond via message intent class. + intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, + Uri.fromParts(SCHEME_SMSTO, "", null)); + List<ResolveInfo> respondServices = packageManager.queryIntentServicesAsUser(intent, 0, + userId); + for (ResolveInfo resolveInfo : respondServices) { + final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + if (serviceInfo == null) { + continue; + } + if (!permission.SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) { + continue; + } + final String packageName = serviceInfo.packageName; + final SmsApplicationData smsApplicationData = receivers.get(packageName); + if (smsApplicationData != null) { + smsApplicationData.mRespondViaMessageClass = serviceInfo.name; + } + } + + // Update any existing entries with supports send to. + intent = new Intent(Intent.ACTION_SENDTO, + Uri.fromParts(SCHEME_SMSTO, "", null)); + List<ResolveInfo> sendToActivities = packageManager.queryIntentActivitiesAsUser(intent, 0, + userId); + for (ResolveInfo resolveInfo : sendToActivities) { + final ActivityInfo activityInfo = resolveInfo.activityInfo; + if (activityInfo == null) { + continue; + } + final String packageName = activityInfo.packageName; + final SmsApplicationData smsApplicationData = receivers.get(packageName); + if (smsApplicationData != null) { + smsApplicationData.mSendToClass = activityInfo.name; + } + } + + // Update any existing entries with the default sms changed handler. + intent = new Intent(Telephony.Sms.Intents.ACTION_DEFAULT_SMS_PACKAGE_CHANGED); + List<ResolveInfo> smsAppChangedReceivers = packageManager.queryBroadcastReceiversAsUser(intent, + 0, userId); + if (DEBUG_MULTIUSER) { + Log.i(LOG_TAG, "getApplicationCollectionInternal smsAppChangedActivities=" + + smsAppChangedReceivers); + } + for (ResolveInfo resolveInfo : smsAppChangedReceivers) { + final ActivityInfo activityInfo = resolveInfo.activityInfo; + if (activityInfo == null) { + continue; + } + final String packageName = activityInfo.packageName; + final SmsApplicationData smsApplicationData = receivers.get(packageName); + if (DEBUG_MULTIUSER) { + Log.i(LOG_TAG, "getApplicationCollectionInternal packageName=" + + packageName + " smsApplicationData: " + smsApplicationData + + " activityInfo.name: " + activityInfo.name); + } + if (smsApplicationData != null) { + smsApplicationData.mSmsAppChangedReceiverClass = activityInfo.name; + } + } + + // Update any existing entries with the external provider changed handler. + intent = new Intent(Telephony.Sms.Intents.ACTION_EXTERNAL_PROVIDER_CHANGE); + List<ResolveInfo> providerChangedReceivers = packageManager.queryBroadcastReceiversAsUser(intent, + 0, userId); + if (DEBUG_MULTIUSER) { + Log.i(LOG_TAG, "getApplicationCollectionInternal providerChangedActivities=" + + providerChangedReceivers); + } + for (ResolveInfo resolveInfo : providerChangedReceivers) { + final ActivityInfo activityInfo = resolveInfo.activityInfo; + if (activityInfo == null) { + continue; + } + final String packageName = activityInfo.packageName; + final SmsApplicationData smsApplicationData = receivers.get(packageName); + if (DEBUG_MULTIUSER) { + Log.i(LOG_TAG, "getApplicationCollectionInternal packageName=" + + packageName + " smsApplicationData: " + smsApplicationData + + " activityInfo.name: " + activityInfo.name); + } + if (smsApplicationData != null) { + smsApplicationData.mProviderChangedReceiverClass = activityInfo.name; + } + } + + // Remove any entries for which we did not find all required intents. + for (ResolveInfo resolveInfo : smsReceivers) { + final ActivityInfo activityInfo = resolveInfo.activityInfo; + if (activityInfo == null) { + continue; + } + final String packageName = activityInfo.packageName; + final SmsApplicationData smsApplicationData = receivers.get(packageName); + if (smsApplicationData != null) { + if (!smsApplicationData.isComplete()) { + receivers.remove(packageName); + } + } + } + return receivers.values(); + } + + /** + * Checks to see if we have a valid installed SMS application for the specified package name + * @return Data for the specified package name or null if there isn't one + */ + private static SmsApplicationData getApplicationForPackage( + Collection<SmsApplicationData> applications, String packageName) { + if (packageName == null) { + return null; + } + // Is there an entry in the application list for the specified package? + for (SmsApplicationData application : applications) { + if (application.mPackageName.contentEquals(packageName)) { + return application; + } + } + return null; + } + + /** + * Get the application we will use for delivering SMS/MMS messages. + * + * We return the preferred sms application with the following order of preference: + * (1) User selected SMS app (if selected, and if still valid) + * (2) Android Messaging (if installed) + * (3) The currently configured highest priority broadcast receiver + * (4) Null + */ + private static SmsApplicationData getApplication(Context context, boolean updateIfNeeded, + int userId) { + TelephonyManager tm = (TelephonyManager) + context.getSystemService(Context.TELEPHONY_SERVICE); + if (!tm.isSmsCapable()) { + // No phone, no SMS + return null; + } + + Collection<SmsApplicationData> applications = getApplicationCollectionInternal(context, + userId); + if (DEBUG_MULTIUSER) { + Log.i(LOG_TAG, "getApplication userId=" + userId); + } + // Determine which application receives the broadcast + String defaultApplication = Settings.Secure.getStringForUser(context.getContentResolver(), + Settings.Secure.SMS_DEFAULT_APPLICATION, userId); + if (DEBUG_MULTIUSER) { + Log.i(LOG_TAG, "getApplication defaultApp=" + defaultApplication); + } + + SmsApplicationData applicationData = null; + if (defaultApplication != null) { + applicationData = getApplicationForPackage(applications, defaultApplication); + } + if (DEBUG_MULTIUSER) { + Log.i(LOG_TAG, "getApplication appData=" + applicationData); + } + // Picking a new SMS app requires AppOps and Settings.Secure permissions, so we only do + // this if the caller asked us to. + if (updateIfNeeded && applicationData == null) { + // Try to find the default SMS package for this device + Resources r = context.getResources(); + String defaultPackage = + r.getString(com.android.internal.R.string.default_sms_application); + applicationData = getApplicationForPackage(applications, defaultPackage); + + if (applicationData == null) { + // Are there any applications? + if (applications.size() != 0) { + applicationData = (SmsApplicationData)applications.toArray()[0]; + } + } + + // If we found a new default app, update the setting + if (applicationData != null) { + setDefaultApplicationInternal(applicationData.mPackageName, context, userId); + } + } + + // If we found a package, make sure AppOps permissions are set up correctly + if (applicationData != null) { + AppOpsManager appOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE); + + // We can only call checkOp if we are privileged (updateIfNeeded) or if the app we + // are checking is for our current uid. Doing this check from the unprivileged current + // SMS app allows us to tell the current SMS app that it is not in a good state and + // needs to ask to be the current SMS app again to work properly. + if (updateIfNeeded || applicationData.mUid == android.os.Process.myUid()) { + // Verify that the SMS app has permissions + int mode = appOps.checkOp(AppOpsManager.OP_WRITE_SMS, applicationData.mUid, + applicationData.mPackageName); + if (mode != AppOpsManager.MODE_ALLOWED) { + Rlog.e(LOG_TAG, applicationData.mPackageName + " lost OP_WRITE_SMS: " + + (updateIfNeeded ? " (fixing)" : " (no permission to fix)")); + if (updateIfNeeded) { + appOps.setMode(AppOpsManager.OP_WRITE_SMS, applicationData.mUid, + applicationData.mPackageName, AppOpsManager.MODE_ALLOWED); + } else { + // We can not return a package if permissions are not set up correctly + applicationData = null; + } + } + } + + // We can only verify the phone and BT app's permissions from a privileged caller + if (updateIfNeeded) { + // Ensure this component is still configured as the preferred activity. Usually the + // current SMS app will already be the preferred activity - but checking whether or + // not this is true is just as expensive as reconfiguring the preferred activity so + // we just reconfigure every time. + PackageManager packageManager = context.getPackageManager(); + configurePreferredActivity(packageManager, new ComponentName( + applicationData.mPackageName, applicationData.mSendToClass), + userId); + // Assign permission to special system apps + assignWriteSmsPermissionToSystemApp(context, packageManager, appOps, + PHONE_PACKAGE_NAME); + assignWriteSmsPermissionToSystemApp(context, packageManager, appOps, + BLUETOOTH_PACKAGE_NAME); + assignWriteSmsPermissionToSystemApp(context, packageManager, appOps, + MMS_SERVICE_PACKAGE_NAME); + assignWriteSmsPermissionToSystemApp(context, packageManager, appOps, + TELEPHONY_PROVIDER_PACKAGE_NAME); + // Give WRITE_SMS AppOps permission to UID 1001 which contains multiple + // apps, all of them should be able to write to telephony provider. + // This is to allow the proxy package permission check in telephony provider + // to pass. + assignWriteSmsPermissionToSystemUid(appOps, Process.PHONE_UID); + } + } + if (DEBUG_MULTIUSER) { + Log.i(LOG_TAG, "getApplication returning appData=" + applicationData); + } + return applicationData; + } + + /** + * Sets the specified package as the default SMS/MMS application. The caller of this method + * needs to have permission to set AppOps and write to secure settings. + */ + public static void setDefaultApplication(String packageName, Context context) { + TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); + if (!tm.isSmsCapable()) { + // No phone, no SMS + return; + } + + final int userId = getIncomingUserId(context); + final long token = Binder.clearCallingIdentity(); + try { + setDefaultApplicationInternal(packageName, context, userId); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private static void setDefaultApplicationInternal(String packageName, Context context, + int userId) { + // Get old package name + String oldPackageName = Settings.Secure.getStringForUser(context.getContentResolver(), + Settings.Secure.SMS_DEFAULT_APPLICATION, userId); + + if (DEBUG_MULTIUSER) { + Log.i(LOG_TAG, "setDefaultApplicationInternal old=" + oldPackageName + + " new=" + packageName); + } + + if (packageName != null && oldPackageName != null && packageName.equals(oldPackageName)) { + // No change + return; + } + + // We only make the change if the new package is valid + PackageManager packageManager = context.getPackageManager(); + Collection<SmsApplicationData> applications = getApplicationCollection(context); + SmsApplicationData oldAppData = oldPackageName != null ? + getApplicationForPackage(applications, oldPackageName) : null; + SmsApplicationData applicationData = getApplicationForPackage(applications, packageName); + if (applicationData != null) { + // Ignore OP_WRITE_SMS for the previously configured default SMS app. + AppOpsManager appOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE); + if (oldPackageName != null) { + try { + PackageInfo info = packageManager.getPackageInfo(oldPackageName, + PackageManager.GET_UNINSTALLED_PACKAGES); + appOps.setMode(AppOpsManager.OP_WRITE_SMS, info.applicationInfo.uid, + oldPackageName, AppOpsManager.MODE_IGNORED); + } catch (NameNotFoundException e) { + Rlog.w(LOG_TAG, "Old SMS package not found: " + oldPackageName); + } + } + + // Update the secure setting. + Settings.Secure.putStringForUser(context.getContentResolver(), + Settings.Secure.SMS_DEFAULT_APPLICATION, applicationData.mPackageName, + userId); + + // Configure this as the preferred activity for SENDTO sms/mms intents + configurePreferredActivity(packageManager, new ComponentName( + applicationData.mPackageName, applicationData.mSendToClass), userId); + + // Allow OP_WRITE_SMS for the newly configured default SMS app. + appOps.setMode(AppOpsManager.OP_WRITE_SMS, applicationData.mUid, + applicationData.mPackageName, AppOpsManager.MODE_ALLOWED); + + // Assign permission to special system apps + assignWriteSmsPermissionToSystemApp(context, packageManager, appOps, + PHONE_PACKAGE_NAME); + assignWriteSmsPermissionToSystemApp(context, packageManager, appOps, + BLUETOOTH_PACKAGE_NAME); + assignWriteSmsPermissionToSystemApp(context, packageManager, appOps, + MMS_SERVICE_PACKAGE_NAME); + assignWriteSmsPermissionToSystemApp(context, packageManager, appOps, + TELEPHONY_PROVIDER_PACKAGE_NAME); + // Give WRITE_SMS AppOps permission to UID 1001 which contains multiple + // apps, all of them should be able to write to telephony provider. + // This is to allow the proxy package permission check in telephony provider + // to pass. + assignWriteSmsPermissionToSystemUid(appOps, Process.PHONE_UID); + + if (DEBUG_MULTIUSER) { + Log.i(LOG_TAG, "setDefaultApplicationInternal oldAppData=" + oldAppData); + } + if (oldAppData != null && oldAppData.mSmsAppChangedReceiverClass != null) { + // Notify the old sms app that it's no longer the default + final Intent oldAppIntent = + new Intent(Telephony.Sms.Intents.ACTION_DEFAULT_SMS_PACKAGE_CHANGED); + final ComponentName component = new ComponentName(oldAppData.mPackageName, + oldAppData.mSmsAppChangedReceiverClass); + oldAppIntent.setComponent(component); + oldAppIntent.putExtra(Telephony.Sms.Intents.EXTRA_IS_DEFAULT_SMS_APP, false); + if (DEBUG_MULTIUSER) { + Log.i(LOG_TAG, "setDefaultApplicationInternal old=" + oldAppData.mPackageName); + } + context.sendBroadcast(oldAppIntent); + } + // Notify the new sms app that it's now the default (if the new sms app has a receiver + // to handle the changed default sms intent). + if (DEBUG_MULTIUSER) { + Log.i(LOG_TAG, "setDefaultApplicationInternal new applicationData=" + + applicationData); + } + if (applicationData.mSmsAppChangedReceiverClass != null) { + final Intent intent = + new Intent(Telephony.Sms.Intents.ACTION_DEFAULT_SMS_PACKAGE_CHANGED); + final ComponentName component = new ComponentName(applicationData.mPackageName, + applicationData.mSmsAppChangedReceiverClass); + intent.setComponent(component); + intent.putExtra(Telephony.Sms.Intents.EXTRA_IS_DEFAULT_SMS_APP, true); + if (DEBUG_MULTIUSER) { + Log.i(LOG_TAG, "setDefaultApplicationInternal new=" + packageName); + } + context.sendBroadcast(intent); + } + MetricsLogger.action(context, MetricsEvent.ACTION_DEFAULT_SMS_APP_CHANGED, + applicationData.mPackageName); + } + } + + /** + * Assign WRITE_SMS AppOps permission to some special system apps. + * + * @param context The context + * @param packageManager The package manager instance + * @param appOps The AppOps manager instance + * @param packageName The package name of the system app + */ + private static void assignWriteSmsPermissionToSystemApp(Context context, + PackageManager packageManager, AppOpsManager appOps, String packageName) { + // First check package signature matches the caller's package signature. + // Since this class is only used internally by the system, this check makes sure + // the package signature matches system signature. + final int result = packageManager.checkSignatures(context.getPackageName(), packageName); + if (result != PackageManager.SIGNATURE_MATCH) { + Rlog.e(LOG_TAG, packageName + " does not have system signature"); + return; + } + try { + PackageInfo info = packageManager.getPackageInfo(packageName, 0); + int mode = appOps.checkOp(AppOpsManager.OP_WRITE_SMS, info.applicationInfo.uid, + packageName); + if (mode != AppOpsManager.MODE_ALLOWED) { + Rlog.w(LOG_TAG, packageName + " does not have OP_WRITE_SMS: (fixing)"); + appOps.setMode(AppOpsManager.OP_WRITE_SMS, info.applicationInfo.uid, + packageName, AppOpsManager.MODE_ALLOWED); + } + } catch (NameNotFoundException e) { + // No whitelisted system app on this device + Rlog.e(LOG_TAG, "Package not found: " + packageName); + } + + } + + private static void assignWriteSmsPermissionToSystemUid(AppOpsManager appOps, int uid) { + appOps.setUidMode(AppOpsManager.OP_WRITE_SMS, uid, AppOpsManager.MODE_ALLOWED); + } + + /** + * Tracks package changes and ensures that the default SMS app is always configured to be the + * preferred activity for SENDTO sms/mms intents. + */ + private static final class SmsPackageMonitor extends PackageMonitor { + final Context mContext; + + public SmsPackageMonitor(Context context) { + super(); + mContext = context; + } + + @Override + public void onPackageDisappeared(String packageName, int reason) { + onPackageChanged(); + } + + @Override + public void onPackageAppeared(String packageName, int reason) { + onPackageChanged(); + } + + @Override + public void onPackageModified(String packageName) { + onPackageChanged(); + } + + private void onPackageChanged() { + PackageManager packageManager = mContext.getPackageManager(); + Context userContext = mContext; + final int userId = getSendingUserId(); + if (userId != UserHandle.USER_SYSTEM) { + try { + userContext = mContext.createPackageContextAsUser(mContext.getPackageName(), 0, + new UserHandle(userId)); + } catch (NameNotFoundException nnfe) { + if (DEBUG_MULTIUSER) { + Log.w(LOG_TAG, "Unable to create package context for user " + userId); + } + } + } + // Ensure this component is still configured as the preferred activity + ComponentName componentName = getDefaultSendToApplication(userContext, true); + if (componentName != null) { + configurePreferredActivity(packageManager, componentName, userId); + } + } + } + + public static void initSmsPackageMonitor(Context context) { + sSmsPackageMonitor = new SmsPackageMonitor(context); + sSmsPackageMonitor.register(context, context.getMainLooper(), UserHandle.ALL, false); + } + + private static void configurePreferredActivity(PackageManager packageManager, + ComponentName componentName, int userId) { + // Add the four activity preferences we want to direct to this app. + replacePreferredActivity(packageManager, componentName, userId, SCHEME_SMS); + replacePreferredActivity(packageManager, componentName, userId, SCHEME_SMSTO); + replacePreferredActivity(packageManager, componentName, userId, SCHEME_MMS); + replacePreferredActivity(packageManager, componentName, userId, SCHEME_MMSTO); + } + + /** + * Updates the ACTION_SENDTO activity to the specified component for the specified scheme. + */ + private static void replacePreferredActivity(PackageManager packageManager, + ComponentName componentName, int userId, String scheme) { + // Build the set of existing activities that handle this scheme + Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts(scheme, "", null)); + List<ResolveInfo> resolveInfoList = packageManager.queryIntentActivitiesAsUser( + intent, PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_RESOLVED_FILTER, + userId); + + // Build the set of ComponentNames for these activities + final int n = resolveInfoList.size(); + ComponentName[] set = new ComponentName[n]; + for (int i = 0; i < n; i++) { + ResolveInfo info = resolveInfoList.get(i); + set[i] = new ComponentName(info.activityInfo.packageName, info.activityInfo.name); + } + + // Update the preferred SENDTO activity for the specified scheme + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_SENDTO); + intentFilter.addCategory(Intent.CATEGORY_DEFAULT); + intentFilter.addDataScheme(scheme); + packageManager.replacePreferredActivityAsUser(intentFilter, + IntentFilter.MATCH_CATEGORY_SCHEME + IntentFilter.MATCH_ADJUSTMENT_NORMAL, + set, componentName, userId); + } + + /** + * Returns SmsApplicationData for this package if this package is capable of being set as the + * default SMS application. + */ + public static SmsApplicationData getSmsApplicationData(String packageName, Context context) { + Collection<SmsApplicationData> applications = getApplicationCollection(context); + return getApplicationForPackage(applications, packageName); + } + + /** + * Gets the default SMS application + * @param context context from the calling app + * @param updateIfNeeded update the default app if there is no valid default app configured. + * @return component name of the app and class to deliver SMS messages to + */ + public static ComponentName getDefaultSmsApplication(Context context, boolean updateIfNeeded) { + int userId = getIncomingUserId(context); + final long token = Binder.clearCallingIdentity(); + try { + ComponentName component = null; + SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded, + userId); + if (smsApplicationData != null) { + component = new ComponentName(smsApplicationData.mPackageName, + smsApplicationData.mSmsReceiverClass); + } + return component; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Gets the default MMS application + * @param context context from the calling app + * @param updateIfNeeded update the default app if there is no valid default app configured. + * @return component name of the app and class to deliver MMS messages to + */ + public static ComponentName getDefaultMmsApplication(Context context, boolean updateIfNeeded) { + int userId = getIncomingUserId(context); + final long token = Binder.clearCallingIdentity(); + try { + ComponentName component = null; + SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded, + userId); + if (smsApplicationData != null) { + component = new ComponentName(smsApplicationData.mPackageName, + smsApplicationData.mMmsReceiverClass); + } + return component; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Gets the default Respond Via Message application + * @param context context from the calling app + * @param updateIfNeeded update the default app if there is no valid default app configured. + * @return component name of the app and class to direct Respond Via Message intent to + */ + public static ComponentName getDefaultRespondViaMessageApplication(Context context, + boolean updateIfNeeded) { + int userId = getIncomingUserId(context); + final long token = Binder.clearCallingIdentity(); + try { + ComponentName component = null; + SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded, + userId); + if (smsApplicationData != null) { + component = new ComponentName(smsApplicationData.mPackageName, + smsApplicationData.mRespondViaMessageClass); + } + return component; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Gets the default Send To (smsto) application. + * <p> + * Caller must pass in the correct user context if calling from a singleton service. + * @param context context from the calling app + * @param updateIfNeeded update the default app if there is no valid default app configured. + * @return component name of the app and class to direct SEND_TO (smsto) intent to + */ + public static ComponentName getDefaultSendToApplication(Context context, + boolean updateIfNeeded) { + int userId = getIncomingUserId(context); + final long token = Binder.clearCallingIdentity(); + try { + ComponentName component = null; + SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded, + userId); + if (smsApplicationData != null) { + component = new ComponentName(smsApplicationData.mPackageName, + smsApplicationData.mSendToClass); + } + return component; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Gets the default application that handles external changes to the SmsProvider and + * MmsProvider. + * @param context context from the calling app + * @param updateIfNeeded update the default app if there is no valid default app configured. + * @return component name of the app and class to deliver change intents to + */ + public static ComponentName getDefaultExternalTelephonyProviderChangedApplication( + Context context, boolean updateIfNeeded) { + int userId = getIncomingUserId(context); + final long token = Binder.clearCallingIdentity(); + try { + ComponentName component = null; + SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded, + userId); + if (smsApplicationData != null + && smsApplicationData.mProviderChangedReceiverClass != null) { + component = new ComponentName(smsApplicationData.mPackageName, + smsApplicationData.mProviderChangedReceiverClass); + } + return component; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Returns whether need to write the SMS message to SMS database for this package. + * <p> + * Caller must pass in the correct user context if calling from a singleton service. + */ + public static boolean shouldWriteMessageForPackage(String packageName, Context context) { + if (SmsManager.getDefault().getAutoPersisting()) { + return true; + } + return !isDefaultSmsApplication(context, packageName); + } + + /** + * Check if a package is default sms app (or equivalent, like bluetooth) + * + * @param context context from the calling app + * @param packageName the name of the package to be checked + * @return true if the package is default sms app or bluetooth + */ + public static boolean isDefaultSmsApplication(Context context, String packageName) { + if (packageName == null) { + return false; + } + final String defaultSmsPackage = getDefaultSmsApplicationPackageName(context); + if ((defaultSmsPackage != null && defaultSmsPackage.equals(packageName)) + || BLUETOOTH_PACKAGE_NAME.equals(packageName)) { + return true; + } + return false; + } + + private static String getDefaultSmsApplicationPackageName(Context context) { + final ComponentName component = getDefaultSmsApplication(context, false); + if (component != null) { + return component.getPackageName(); + } + return null; + } +} diff --git a/telephony/java/com/android/internal/telephony/SmsCbCmasInfo.java b/telephony/java/com/android/internal/telephony/SmsCbCmasInfo.java new file mode 100644 index 000000000000..c912924424b1 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/SmsCbCmasInfo.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2012 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.telephony; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Contains CMAS warning notification Type 1 elements for a {@link SmsCbMessage}. + * Supported values for each element are defined in TIA-1149-0-1 (CMAS over CDMA) and + * 3GPP TS 23.041 (for GSM/UMTS). + * + * {@hide} + */ +public class SmsCbCmasInfo implements Parcelable { + + // CMAS message class (in GSM/UMTS message identifier or CDMA service category). + + /** Presidential-level alert (Korean Public Alert System Class 0 message). */ + public static final int CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT = 0x00; + + /** Extreme threat to life and property (Korean Public Alert System Class 1 message). */ + public static final int CMAS_CLASS_EXTREME_THREAT = 0x01; + + /** Severe threat to life and property (Korean Public Alert System Class 1 message). */ + public static final int CMAS_CLASS_SEVERE_THREAT = 0x02; + + /** Child abduction emergency (AMBER Alert). */ + public static final int CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY = 0x03; + + /** CMAS test message. */ + public static final int CMAS_CLASS_REQUIRED_MONTHLY_TEST = 0x04; + + /** CMAS exercise. */ + public static final int CMAS_CLASS_CMAS_EXERCISE = 0x05; + + /** CMAS category for operator defined use. */ + public static final int CMAS_CLASS_OPERATOR_DEFINED_USE = 0x06; + + /** CMAS category for warning types that are reserved for future extension. */ + public static final int CMAS_CLASS_UNKNOWN = -1; + + // CMAS alert category (in CDMA type 1 elements record). + + /** CMAS alert category: Geophysical including landslide. */ + public static final int CMAS_CATEGORY_GEO = 0x00; + + /** CMAS alert category: Meteorological including flood. */ + public static final int CMAS_CATEGORY_MET = 0x01; + + /** CMAS alert category: General emergency and public safety. */ + public static final int CMAS_CATEGORY_SAFETY = 0x02; + + /** CMAS alert category: Law enforcement, military, homeland/local/private security. */ + public static final int CMAS_CATEGORY_SECURITY = 0x03; + + /** CMAS alert category: Rescue and recovery. */ + public static final int CMAS_CATEGORY_RESCUE = 0x04; + + /** CMAS alert category: Fire suppression and rescue. */ + public static final int CMAS_CATEGORY_FIRE = 0x05; + + /** CMAS alert category: Medical and public health. */ + public static final int CMAS_CATEGORY_HEALTH = 0x06; + + /** CMAS alert category: Pollution and other environmental. */ + public static final int CMAS_CATEGORY_ENV = 0x07; + + /** CMAS alert category: Public and private transportation. */ + public static final int CMAS_CATEGORY_TRANSPORT = 0x08; + + /** CMAS alert category: Utility, telecom, other non-transport infrastructure. */ + public static final int CMAS_CATEGORY_INFRA = 0x09; + + /** CMAS alert category: Chem, bio, radiological, nuclear, high explosive threat or attack. */ + public static final int CMAS_CATEGORY_CBRNE = 0x0a; + + /** CMAS alert category: Other events. */ + public static final int CMAS_CATEGORY_OTHER = 0x0b; + + /** + * CMAS alert category is unknown. The category is only available for CDMA broadcasts + * containing a type 1 elements record, so GSM and UMTS broadcasts always return unknown. + */ + public static final int CMAS_CATEGORY_UNKNOWN = -1; + + // CMAS response type (in CDMA type 1 elements record). + + /** CMAS response type: Take shelter in place. */ + public static final int CMAS_RESPONSE_TYPE_SHELTER = 0x00; + + /** CMAS response type: Evacuate (Relocate). */ + public static final int CMAS_RESPONSE_TYPE_EVACUATE = 0x01; + + /** CMAS response type: Make preparations. */ + public static final int CMAS_RESPONSE_TYPE_PREPARE = 0x02; + + /** CMAS response type: Execute a pre-planned activity. */ + public static final int CMAS_RESPONSE_TYPE_EXECUTE = 0x03; + + /** CMAS response type: Attend to information sources. */ + public static final int CMAS_RESPONSE_TYPE_MONITOR = 0x04; + + /** CMAS response type: Avoid hazard. */ + public static final int CMAS_RESPONSE_TYPE_AVOID = 0x05; + + /** CMAS response type: Evaluate the information in this message (not for public warnings). */ + public static final int CMAS_RESPONSE_TYPE_ASSESS = 0x06; + + /** CMAS response type: No action recommended. */ + public static final int CMAS_RESPONSE_TYPE_NONE = 0x07; + + /** + * CMAS response type is unknown. The response type is only available for CDMA broadcasts + * containing a type 1 elements record, so GSM and UMTS broadcasts always return unknown. + */ + public static final int CMAS_RESPONSE_TYPE_UNKNOWN = -1; + + // 4-bit CMAS severity (in GSM/UMTS message identifier or CDMA type 1 elements record). + + /** CMAS severity type: Extraordinary threat to life or property. */ + public static final int CMAS_SEVERITY_EXTREME = 0x0; + + /** CMAS severity type: Significant threat to life or property. */ + public static final int CMAS_SEVERITY_SEVERE = 0x1; + + /** + * CMAS alert severity is unknown. The severity is available for CDMA warning alerts + * containing a type 1 elements record and for all GSM and UMTS alerts except for the + * Presidential-level alert class (Korean Public Alert System Class 0). + */ + public static final int CMAS_SEVERITY_UNKNOWN = -1; + + // CMAS urgency (in GSM/UMTS message identifier or CDMA type 1 elements record). + + /** CMAS urgency type: Responsive action should be taken immediately. */ + public static final int CMAS_URGENCY_IMMEDIATE = 0x0; + + /** CMAS urgency type: Responsive action should be taken within the next hour. */ + public static final int CMAS_URGENCY_EXPECTED = 0x1; + + /** + * CMAS alert urgency is unknown. The urgency is available for CDMA warning alerts + * containing a type 1 elements record and for all GSM and UMTS alerts except for the + * Presidential-level alert class (Korean Public Alert System Class 0). + */ + public static final int CMAS_URGENCY_UNKNOWN = -1; + + // CMAS certainty (in GSM/UMTS message identifier or CDMA type 1 elements record). + + /** CMAS certainty type: Determined to have occurred or to be ongoing. */ + public static final int CMAS_CERTAINTY_OBSERVED = 0x0; + + /** CMAS certainty type: Likely (probability > ~50%). */ + public static final int CMAS_CERTAINTY_LIKELY = 0x1; + + /** + * CMAS alert certainty is unknown. The certainty is available for CDMA warning alerts + * containing a type 1 elements record and for all GSM and UMTS alerts except for the + * Presidential-level alert class (Korean Public Alert System Class 0). + */ + public static final int CMAS_CERTAINTY_UNKNOWN = -1; + + /** CMAS message class. */ + private final int mMessageClass; + + /** CMAS category. */ + private final int mCategory; + + /** CMAS response type. */ + private final int mResponseType; + + /** CMAS severity. */ + private final int mSeverity; + + /** CMAS urgency. */ + private final int mUrgency; + + /** CMAS certainty. */ + private final int mCertainty; + + /** Create a new SmsCbCmasInfo object with the specified values. */ + public SmsCbCmasInfo(int messageClass, int category, int responseType, int severity, + int urgency, int certainty) { + mMessageClass = messageClass; + mCategory = category; + mResponseType = responseType; + mSeverity = severity; + mUrgency = urgency; + mCertainty = certainty; + } + + /** Create a new SmsCbCmasInfo object from a Parcel. */ + SmsCbCmasInfo(Parcel in) { + mMessageClass = in.readInt(); + mCategory = in.readInt(); + mResponseType = in.readInt(); + mSeverity = in.readInt(); + mUrgency = in.readInt(); + mCertainty = in.readInt(); + } + + /** + * Flatten this object into a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written (ignored). + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mMessageClass); + dest.writeInt(mCategory); + dest.writeInt(mResponseType); + dest.writeInt(mSeverity); + dest.writeInt(mUrgency); + dest.writeInt(mCertainty); + } + + /** + * Returns the CMAS message class, e.g. {@link #CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT}. + * @return one of the {@code CMAS_CLASS} values + */ + public int getMessageClass() { + return mMessageClass; + } + + /** + * Returns the CMAS category, e.g. {@link #CMAS_CATEGORY_GEO}. + * @return one of the {@code CMAS_CATEGORY} values + */ + public int getCategory() { + return mCategory; + } + + /** + * Returns the CMAS response type, e.g. {@link #CMAS_RESPONSE_TYPE_SHELTER}. + * @return one of the {@code CMAS_RESPONSE_TYPE} values + */ + public int getResponseType() { + return mResponseType; + } + + /** + * Returns the CMAS severity, e.g. {@link #CMAS_SEVERITY_EXTREME}. + * @return one of the {@code CMAS_SEVERITY} values + */ + public int getSeverity() { + return mSeverity; + } + + /** + * Returns the CMAS urgency, e.g. {@link #CMAS_URGENCY_IMMEDIATE}. + * @return one of the {@code CMAS_URGENCY} values + */ + public int getUrgency() { + return mUrgency; + } + + /** + * Returns the CMAS certainty, e.g. {@link #CMAS_CERTAINTY_OBSERVED}. + * @return one of the {@code CMAS_CERTAINTY} values + */ + public int getCertainty() { + return mCertainty; + } + + @Override + public String toString() { + return "SmsCbCmasInfo{messageClass=" + mMessageClass + ", category=" + mCategory + + ", responseType=" + mResponseType + ", severity=" + mSeverity + + ", urgency=" + mUrgency + ", certainty=" + mCertainty + '}'; + } + + /** + * Describe the kinds of special objects contained in the marshalled representation. + * @return a bitmask indicating this Parcelable contains no special objects + */ + @Override + public int describeContents() { + return 0; + } + + /** Creator for unparcelling objects. */ + public static final Parcelable.Creator<SmsCbCmasInfo> + CREATOR = new Parcelable.Creator<SmsCbCmasInfo>() { + @Override + public SmsCbCmasInfo createFromParcel(Parcel in) { + return new SmsCbCmasInfo(in); + } + + @Override + public SmsCbCmasInfo[] newArray(int size) { + return new SmsCbCmasInfo[size]; + } + }; +} diff --git a/telephony/java/com/android/internal/telephony/SmsCbEtwsInfo.java b/telephony/java/com/android/internal/telephony/SmsCbEtwsInfo.java new file mode 100644 index 000000000000..14e02de276fd --- /dev/null +++ b/telephony/java/com/android/internal/telephony/SmsCbEtwsInfo.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2012 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.telephony; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.format.Time; + +import com.android.internal.telephony.uicc.IccUtils; + +import java.util.Arrays; + +/** + * Contains information elements for a GSM or UMTS ETWS warning notification. + * Supported values for each element are defined in 3GPP TS 23.041. + * + * {@hide} + */ +public class SmsCbEtwsInfo implements Parcelable { + + /** ETWS warning type for earthquake. */ + public static final int ETWS_WARNING_TYPE_EARTHQUAKE = 0x00; + + /** ETWS warning type for tsunami. */ + public static final int ETWS_WARNING_TYPE_TSUNAMI = 0x01; + + /** ETWS warning type for earthquake and tsunami. */ + public static final int ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI = 0x02; + + /** ETWS warning type for test messages. */ + public static final int ETWS_WARNING_TYPE_TEST_MESSAGE = 0x03; + + /** ETWS warning type for other emergency types. */ + public static final int ETWS_WARNING_TYPE_OTHER_EMERGENCY = 0x04; + + /** Unknown ETWS warning type. */ + public static final int ETWS_WARNING_TYPE_UNKNOWN = -1; + + /** One of the ETWS warning type constants defined in this class. */ + private final int mWarningType; + + /** Whether or not to activate the emergency user alert tone and vibration. */ + private final boolean mEmergencyUserAlert; + + /** Whether or not to activate a popup alert. */ + private final boolean mActivatePopup; + + /** Whether ETWS primary message or not/ */ + private final boolean mPrimary; + + /** + * 50-byte security information (ETWS primary notification for GSM only). As of Release 10, + * 3GPP TS 23.041 states that the UE shall ignore the ETWS primary notification timestamp + * and digital signature if received. Therefore it is treated as a raw byte array and + * parceled with the broadcast intent if present, but the timestamp is only computed if an + * application asks for the individual components. + */ + private final byte[] mWarningSecurityInformation; + + /** Create a new SmsCbEtwsInfo object with the specified values. */ + public SmsCbEtwsInfo(int warningType, boolean emergencyUserAlert, boolean activatePopup, + boolean primary, byte[] warningSecurityInformation) { + mWarningType = warningType; + mEmergencyUserAlert = emergencyUserAlert; + mActivatePopup = activatePopup; + mPrimary = primary; + mWarningSecurityInformation = warningSecurityInformation; + } + + /** Create a new SmsCbEtwsInfo object from a Parcel. */ + SmsCbEtwsInfo(Parcel in) { + mWarningType = in.readInt(); + mEmergencyUserAlert = (in.readInt() != 0); + mActivatePopup = (in.readInt() != 0); + mPrimary = (in.readInt() != 0); + mWarningSecurityInformation = in.createByteArray(); + } + + /** + * Flatten this object into a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written (ignored). + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mWarningType); + dest.writeInt(mEmergencyUserAlert ? 1 : 0); + dest.writeInt(mActivatePopup ? 1 : 0); + dest.writeInt(mPrimary ? 1 : 0); + dest.writeByteArray(mWarningSecurityInformation); + } + + /** + * Returns the ETWS warning type. + * @return a warning type such as {@link #ETWS_WARNING_TYPE_EARTHQUAKE} + */ + public int getWarningType() { + return mWarningType; + } + + /** + * Returns the ETWS emergency user alert flag. + * @return true to notify terminal to activate emergency user alert; false otherwise + */ + public boolean isEmergencyUserAlert() { + return mEmergencyUserAlert; + } + + /** + * Returns the ETWS activate popup flag. + * @return true to notify terminal to activate display popup; false otherwise + */ + public boolean isPopupAlert() { + return mActivatePopup; + } + + /** + * Returns the ETWS format flag. + * @return true if the message is primary message, otherwise secondary message + */ + public boolean isPrimary() { + return mPrimary; + } + + /** + * Returns the Warning-Security-Information timestamp (GSM primary notifications only). + * As of Release 10, 3GPP TS 23.041 states that the UE shall ignore this value if received. + * @return a UTC timestamp in System.currentTimeMillis() format, or 0 if not present + */ + public long getPrimaryNotificationTimestamp() { + if (mWarningSecurityInformation == null || mWarningSecurityInformation.length < 7) { + return 0; + } + + int year = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[0]); + int month = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[1]); + int day = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[2]); + int hour = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[3]); + int minute = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[4]); + int second = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[5]); + + // For the timezone, the most significant bit of the + // least significant nibble is the sign byte + // (meaning the max range of this field is 79 quarter-hours, + // which is more than enough) + + byte tzByte = mWarningSecurityInformation[6]; + + // Mask out sign bit. + int timezoneOffset = IccUtils.gsmBcdByteToInt((byte) (tzByte & (~0x08))); + + timezoneOffset = ((tzByte & 0x08) == 0) ? timezoneOffset : -timezoneOffset; + + Time time = new Time(Time.TIMEZONE_UTC); + + // We only need to support years above 2000. + time.year = year + 2000; + time.month = month - 1; + time.monthDay = day; + time.hour = hour; + time.minute = minute; + time.second = second; + + // Timezone offset is in quarter hours. + return time.toMillis(true) - timezoneOffset * 15 * 60 * 1000; + } + + /** + * Returns the digital signature (GSM primary notifications only). As of Release 10, + * 3GPP TS 23.041 states that the UE shall ignore this value if received. + * @return a byte array containing a copy of the primary notification digital signature + */ + public byte[] getPrimaryNotificationSignature() { + if (mWarningSecurityInformation == null || mWarningSecurityInformation.length < 50) { + return null; + } + return Arrays.copyOfRange(mWarningSecurityInformation, 7, 50); + } + + @Override + public String toString() { + return "SmsCbEtwsInfo{warningType=" + mWarningType + ", emergencyUserAlert=" + + mEmergencyUserAlert + ", activatePopup=" + mActivatePopup + '}'; + } + + /** + * Describe the kinds of special objects contained in the marshalled representation. + * @return a bitmask indicating this Parcelable contains no special objects + */ + @Override + public int describeContents() { + return 0; + } + + /** Creator for unparcelling objects. */ + public static final Creator<SmsCbEtwsInfo> CREATOR = new Creator<SmsCbEtwsInfo>() { + @Override + public SmsCbEtwsInfo createFromParcel(Parcel in) { + return new SmsCbEtwsInfo(in); + } + + @Override + public SmsCbEtwsInfo[] newArray(int size) { + return new SmsCbEtwsInfo[size]; + } + }; +} diff --git a/telephony/java/com/android/internal/telephony/SmsCbLocation.java b/telephony/java/com/android/internal/telephony/SmsCbLocation.java new file mode 100644 index 000000000000..6eb72a86698f --- /dev/null +++ b/telephony/java/com/android/internal/telephony/SmsCbLocation.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2012 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.telephony; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents the location and geographical scope of a cell broadcast message. + * For GSM/UMTS, the Location Area and Cell ID are set when the broadcast + * geographical scope is cell wide or Location Area wide. For CDMA, the + * broadcast geographical scope is always PLMN wide. + * + * @hide + */ +public class SmsCbLocation implements Parcelable { + + /** The PLMN. Note that this field may be an empty string, but isn't allowed to be null. */ + private final String mPlmn; + + private final int mLac; + private final int mCid; + + /** + * Construct an empty location object. This is used for some test cases, and for + * cell broadcasts saved in older versions of the database without location info. + */ + public SmsCbLocation() { + mPlmn = ""; + mLac = -1; + mCid = -1; + } + + /** + * Construct a location object for the PLMN. This class is immutable, so + * the same object can be reused for multiple broadcasts. + */ + public SmsCbLocation(String plmn) { + mPlmn = plmn; + mLac = -1; + mCid = -1; + } + + /** + * Construct a location object for the PLMN, LAC, and Cell ID. This class is immutable, so + * the same object can be reused for multiple broadcasts. + */ + public SmsCbLocation(String plmn, int lac, int cid) { + mPlmn = plmn; + mLac = lac; + mCid = cid; + } + + /** + * Initialize the object from a Parcel. + */ + public SmsCbLocation(Parcel in) { + mPlmn = in.readString(); + mLac = in.readInt(); + mCid = in.readInt(); + } + + /** + * Returns the MCC/MNC of the network as a String. + * @return the PLMN identifier (MCC+MNC) as a String + */ + public String getPlmn() { + return mPlmn; + } + + /** + * Returns the GSM location area code, or UMTS service area code. + * @return location area code, -1 if unknown, 0xffff max legal value + */ + public int getLac() { + return mLac; + } + + /** + * Returns the GSM or UMTS cell ID. + * @return gsm cell id, -1 if unknown, 0xffff max legal value + */ + public int getCid() { + return mCid; + } + + @Override + public int hashCode() { + int hash = mPlmn.hashCode(); + hash = hash * 31 + mLac; + hash = hash * 31 + mCid; + return hash; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || !(o instanceof SmsCbLocation)) { + return false; + } + SmsCbLocation other = (SmsCbLocation) o; + return mPlmn.equals(other.mPlmn) && mLac == other.mLac && mCid == other.mCid; + } + + @Override + public String toString() { + return '[' + mPlmn + ',' + mLac + ',' + mCid + ']'; + } + + /** + * Test whether this location is within the location area of the specified object. + * + * @param area the location area to compare with this location + * @return true if this location is contained within the specified location area + */ + public boolean isInLocationArea(SmsCbLocation area) { + if (mCid != -1 && mCid != area.mCid) { + return false; + } + if (mLac != -1 && mLac != area.mLac) { + return false; + } + return mPlmn.equals(area.mPlmn); + } + + /** + * Test whether this location is within the location area of the CellLocation. + * + * @param plmn the PLMN to use for comparison + * @param lac the Location Area (GSM) or Service Area (UMTS) to compare with + * @param cid the Cell ID to compare with + * @return true if this location is contained within the specified PLMN, LAC, and Cell ID + */ + public boolean isInLocationArea(String plmn, int lac, int cid) { + if (!mPlmn.equals(plmn)) { + return false; + } + + if (mLac != -1 && mLac != lac) { + return false; + } + + if (mCid != -1 && mCid != cid) { + return false; + } + + return true; + } + + /** + * Flatten this object into a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written (ignored). + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mPlmn); + dest.writeInt(mLac); + dest.writeInt(mCid); + } + + public static final Parcelable.Creator<SmsCbLocation> CREATOR + = new Parcelable.Creator<SmsCbLocation>() { + @Override + public SmsCbLocation createFromParcel(Parcel in) { + return new SmsCbLocation(in); + } + + @Override + public SmsCbLocation[] newArray(int size) { + return new SmsCbLocation[size]; + } + }; + + /** + * Describe the kinds of special objects contained in the marshalled representation. + * @return a bitmask indicating this Parcelable contains no special objects + */ + @Override + public int describeContents() { + return 0; + } +} diff --git a/telephony/java/com/android/internal/telephony/SmsCbMessage.java b/telephony/java/com/android/internal/telephony/SmsCbMessage.java new file mode 100644 index 000000000000..046bf8c700eb --- /dev/null +++ b/telephony/java/com/android/internal/telephony/SmsCbMessage.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2010 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.telephony; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Parcelable object containing a received cell broadcast message. There are four different types + * of Cell Broadcast messages: + * + * <ul> + * <li>opt-in informational broadcasts, e.g. news, weather, stock quotes, sports scores</li> + * <li>cell information messages, broadcast on channel 50, indicating the current cell name for + * roaming purposes (required to display on the idle screen in Brazil)</li> + * <li>emergency broadcasts for the Japanese Earthquake and Tsunami Warning System (ETWS)</li> + * <li>emergency broadcasts for the American Commercial Mobile Alert Service (CMAS)</li> + * </ul> + * + * <p>There are also four different CB message formats: GSM, ETWS Primary Notification (GSM only), + * UMTS, and CDMA. Some fields are only applicable for some message formats. Other fields were + * unified under a common name, avoiding some names, such as "Message Identifier", that refer to + * two completely different concepts in 3GPP and CDMA. + * + * <p>The GSM/UMTS Message Identifier field is available via {@link #getServiceCategory}, the name + * of the equivalent field in CDMA. In both cases the service category is a 16-bit value, but 3GPP + * and 3GPP2 have completely different meanings for the respective values. For ETWS and CMAS, the + * application should + * + * <p>The CDMA Message Identifier field is available via {@link #getSerialNumber}, which is used + * to detect the receipt of a duplicate message to be discarded. In CDMA, the message ID is + * unique to the current PLMN. In GSM/UMTS, there is a 16-bit serial number containing a 2-bit + * Geographical Scope field which indicates whether the 10-bit message code and 4-bit update number + * are considered unique to the PLMN, to the current cell, or to the current Location Area (or + * Service Area in UMTS). The relevant values are concatenated into a single String which will be + * unique if the messages are not duplicates. + * + * <p>The SMS dispatcher does not detect duplicate messages. However, it does concatenate the + * pages of a GSM multi-page cell broadcast into a single SmsCbMessage object. + * + * <p>Interested applications with {@code RECEIVE_SMS_PERMISSION} can register to receive + * {@code SMS_CB_RECEIVED_ACTION} broadcast intents for incoming non-emergency broadcasts. + * Only system applications such as the CellBroadcastReceiver may receive notifications for + * emergency broadcasts (ETWS and CMAS). This is intended to prevent any potential for delays or + * interference with the immediate display of the alert message and playing of the alert sound and + * vibration pattern, which could be caused by poorly written or malicious non-system code. + * + * @hide + */ +public class SmsCbMessage implements Parcelable { + + protected static final String LOG_TAG = "SMSCB"; + + /** Cell wide geographical scope with immediate display (GSM/UMTS only). */ + public static final int GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE = 0; + + /** PLMN wide geographical scope (GSM/UMTS and all CDMA broadcasts). */ + public static final int GEOGRAPHICAL_SCOPE_PLMN_WIDE = 1; + + /** Location / service area wide geographical scope (GSM/UMTS only). */ + public static final int GEOGRAPHICAL_SCOPE_LA_WIDE = 2; + + /** Cell wide geographical scope (GSM/UMTS only). */ + public static final int GEOGRAPHICAL_SCOPE_CELL_WIDE = 3; + + /** GSM or UMTS format cell broadcast. */ + public static final int MESSAGE_FORMAT_3GPP = 1; + + /** CDMA format cell broadcast. */ + public static final int MESSAGE_FORMAT_3GPP2 = 2; + + /** Normal message priority. */ + public static final int MESSAGE_PRIORITY_NORMAL = 0; + + /** Interactive message priority. */ + public static final int MESSAGE_PRIORITY_INTERACTIVE = 1; + + /** Urgent message priority. */ + public static final int MESSAGE_PRIORITY_URGENT = 2; + + /** Emergency message priority. */ + public static final int MESSAGE_PRIORITY_EMERGENCY = 3; + + /** Format of this message (for interpretation of service category values). */ + private final int mMessageFormat; + + /** Geographical scope of broadcast. */ + private final int mGeographicalScope; + + /** + * Serial number of broadcast (message identifier for CDMA, geographical scope + message code + + * update number for GSM/UMTS). The serial number plus the location code uniquely identify + * a cell broadcast for duplicate detection. + */ + private final int mSerialNumber; + + /** + * Location identifier for this message. It consists of the current operator MCC/MNC as a + * 5 or 6-digit decimal string. In addition, for GSM/UMTS, if the Geographical Scope of the + * message is not binary 01, the Location Area is included for comparison. If the GS is + * 00 or 11, the Cell ID is also included. LAC and Cell ID are -1 if not specified. + */ + private final SmsCbLocation mLocation; + + /** + * 16-bit CDMA service category or GSM/UMTS message identifier. For ETWS and CMAS warnings, + * the information provided by the category is also available via {@link #getEtwsWarningInfo()} + * or {@link #getCmasWarningInfo()}. + */ + private final int mServiceCategory; + + /** Message language, as a two-character string, e.g. "en". */ + private final String mLanguage; + + /** Message body, as a String. */ + private final String mBody; + + /** Message priority (including emergency priority). */ + private final int mPriority; + + /** ETWS warning notification information (ETWS warnings only). */ + private final SmsCbEtwsInfo mEtwsWarningInfo; + + /** CMAS warning notification information (CMAS warnings only). */ + private final SmsCbCmasInfo mCmasWarningInfo; + + /** + * Create a new SmsCbMessage with the specified data. + */ + public SmsCbMessage(int messageFormat, int geographicalScope, int serialNumber, + SmsCbLocation location, int serviceCategory, String language, String body, + int priority, SmsCbEtwsInfo etwsWarningInfo, SmsCbCmasInfo cmasWarningInfo) { + mMessageFormat = messageFormat; + mGeographicalScope = geographicalScope; + mSerialNumber = serialNumber; + mLocation = location; + mServiceCategory = serviceCategory; + mLanguage = language; + mBody = body; + mPriority = priority; + mEtwsWarningInfo = etwsWarningInfo; + mCmasWarningInfo = cmasWarningInfo; + } + + /** Create a new SmsCbMessage object from a Parcel. */ + public SmsCbMessage(Parcel in) { + mMessageFormat = in.readInt(); + mGeographicalScope = in.readInt(); + mSerialNumber = in.readInt(); + mLocation = new SmsCbLocation(in); + mServiceCategory = in.readInt(); + mLanguage = in.readString(); + mBody = in.readString(); + mPriority = in.readInt(); + int type = in.readInt(); + switch (type) { + case 'E': + // unparcel ETWS warning information + mEtwsWarningInfo = new SmsCbEtwsInfo(in); + mCmasWarningInfo = null; + break; + + case 'C': + // unparcel CMAS warning information + mEtwsWarningInfo = null; + mCmasWarningInfo = new SmsCbCmasInfo(in); + break; + + default: + mEtwsWarningInfo = null; + mCmasWarningInfo = null; + } + } + + /** + * Flatten this object into a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written (ignored). + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mMessageFormat); + dest.writeInt(mGeographicalScope); + dest.writeInt(mSerialNumber); + mLocation.writeToParcel(dest, flags); + dest.writeInt(mServiceCategory); + dest.writeString(mLanguage); + dest.writeString(mBody); + dest.writeInt(mPriority); + if (mEtwsWarningInfo != null) { + // parcel ETWS warning information + dest.writeInt('E'); + mEtwsWarningInfo.writeToParcel(dest, flags); + } else if (mCmasWarningInfo != null) { + // parcel CMAS warning information + dest.writeInt('C'); + mCmasWarningInfo.writeToParcel(dest, flags); + } else { + // no ETWS or CMAS warning information + dest.writeInt('0'); + } + } + + public static final Parcelable.Creator<SmsCbMessage> CREATOR + = new Parcelable.Creator<SmsCbMessage>() { + @Override + public SmsCbMessage createFromParcel(Parcel in) { + return new SmsCbMessage(in); + } + + @Override + public SmsCbMessage[] newArray(int size) { + return new SmsCbMessage[size]; + } + }; + + /** + * Return the geographical scope of this message (GSM/UMTS only). + * + * @return Geographical scope + */ + public int getGeographicalScope() { + return mGeographicalScope; + } + + /** + * Return the broadcast serial number of broadcast (message identifier for CDMA, or + * geographical scope + message code + update number for GSM/UMTS). The serial number plus + * the location code uniquely identify a cell broadcast for duplicate detection. + * + * @return the 16-bit CDMA message identifier or GSM/UMTS serial number + */ + public int getSerialNumber() { + return mSerialNumber; + } + + /** + * Return the location identifier for this message, consisting of the MCC/MNC as a + * 5 or 6-digit decimal string. In addition, for GSM/UMTS, if the Geographical Scope of the + * message is not binary 01, the Location Area is included. If the GS is 00 or 11, the + * cell ID is also included. The {@link SmsCbLocation} object includes a method to test + * if the location is included within another location area or within a PLMN and CellLocation. + * + * @return the geographical location code for duplicate message detection + */ + public SmsCbLocation getLocation() { + return mLocation; + } + + /** + * Return the 16-bit CDMA service category or GSM/UMTS message identifier. The interpretation + * of the category is radio technology specific. For ETWS and CMAS warnings, the information + * provided by the category is available via {@link #getEtwsWarningInfo()} or + * {@link #getCmasWarningInfo()} in a radio technology independent format. + * + * @return the radio technology specific service category + */ + public int getServiceCategory() { + return mServiceCategory; + } + + /** + * Get the ISO-639-1 language code for this message, or null if unspecified + * + * @return Language code + */ + public String getLanguageCode() { + return mLanguage; + } + + /** + * Get the body of this message, or null if no body available + * + * @return Body, or null + */ + public String getMessageBody() { + return mBody; + } + + /** + * Get the message format ({@link #MESSAGE_FORMAT_3GPP} or {@link #MESSAGE_FORMAT_3GPP2}). + * @return an integer representing 3GPP or 3GPP2 message format + */ + public int getMessageFormat() { + return mMessageFormat; + } + + /** + * Get the message priority. Normal broadcasts return {@link #MESSAGE_PRIORITY_NORMAL} + * and emergency broadcasts return {@link #MESSAGE_PRIORITY_EMERGENCY}. CDMA also may return + * {@link #MESSAGE_PRIORITY_INTERACTIVE} or {@link #MESSAGE_PRIORITY_URGENT}. + * @return an integer representing the message priority + */ + public int getMessagePriority() { + return mPriority; + } + + /** + * If this is an ETWS warning notification then this method will return an object containing + * the ETWS warning type, the emergency user alert flag, and the popup flag. If this is an + * ETWS primary notification (GSM only), there will also be a 7-byte timestamp and 43-byte + * digital signature. As of Release 10, 3GPP TS 23.041 states that the UE shall ignore the + * ETWS primary notification timestamp and digital signature if received. + * + * @return an SmsCbEtwsInfo object, or null if this is not an ETWS warning notification + */ + public SmsCbEtwsInfo getEtwsWarningInfo() { + return mEtwsWarningInfo; + } + + /** + * If this is a CMAS warning notification then this method will return an object containing + * the CMAS message class, category, response type, severity, urgency and certainty. + * The message class is always present. Severity, urgency and certainty are present for CDMA + * warning notifications containing a type 1 elements record and for GSM and UMTS warnings + * except for the Presidential-level alert category. Category and response type are only + * available for CDMA notifications containing a type 1 elements record. + * + * @return an SmsCbCmasInfo object, or null if this is not a CMAS warning notification + */ + public SmsCbCmasInfo getCmasWarningInfo() { + return mCmasWarningInfo; + } + + /** + * Return whether this message is an emergency (PWS) message type. + * @return true if the message is a public warning notification; false otherwise + */ + public boolean isEmergencyMessage() { + return mPriority == MESSAGE_PRIORITY_EMERGENCY; + } + + /** + * Return whether this message is an ETWS warning alert. + * @return true if the message is an ETWS warning notification; false otherwise + */ + public boolean isEtwsMessage() { + return mEtwsWarningInfo != null; + } + + /** + * Return whether this message is a CMAS warning alert. + * @return true if the message is a CMAS warning notification; false otherwise + */ + public boolean isCmasMessage() { + return mCmasWarningInfo != null; + } + + @Override + public String toString() { + return "SmsCbMessage{geographicalScope=" + mGeographicalScope + ", serialNumber=" + + mSerialNumber + ", location=" + mLocation + ", serviceCategory=" + + mServiceCategory + ", language=" + mLanguage + ", body=" + mBody + + ", priority=" + mPriority + + (mEtwsWarningInfo != null ? (", " + mEtwsWarningInfo.toString()) : "") + + (mCmasWarningInfo != null ? (", " + mCmasWarningInfo.toString()) : "") + '}'; + } + + /** + * Describe the kinds of special objects contained in the marshalled representation. + * @return a bitmask indicating this Parcelable contains no special objects + */ + @Override + public int describeContents() { + return 0; + } +} diff --git a/telephony/java/com/android/internal/telephony/SmsHeader.java b/telephony/java/com/android/internal/telephony/SmsHeader.java new file mode 100644 index 000000000000..b519b70088fb --- /dev/null +++ b/telephony/java/com/android/internal/telephony/SmsHeader.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2006 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 com.android.internal.telephony; + +import com.android.internal.telephony.SmsConstants; +import com.android.internal.util.HexDump; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import java.util.ArrayList; + +/** + * SMS user data header, as specified in TS 23.040 9.2.3.24. + */ +public class SmsHeader { + + // TODO(cleanup): this data structure is generally referred to as + // the 'user data header' or UDH, and so the class name should + // change to reflect this... + + /** SMS user data header information element identifiers. + * (see TS 23.040 9.2.3.24) + */ + public static final int ELT_ID_CONCATENATED_8_BIT_REFERENCE = 0x00; + public static final int ELT_ID_SPECIAL_SMS_MESSAGE_INDICATION = 0x01; + public static final int ELT_ID_APPLICATION_PORT_ADDRESSING_8_BIT = 0x04; + public static final int ELT_ID_APPLICATION_PORT_ADDRESSING_16_BIT = 0x05; + public static final int ELT_ID_SMSC_CONTROL_PARAMS = 0x06; + public static final int ELT_ID_UDH_SOURCE_INDICATION = 0x07; + public static final int ELT_ID_CONCATENATED_16_BIT_REFERENCE = 0x08; + public static final int ELT_ID_WIRELESS_CTRL_MSG_PROTOCOL = 0x09; + public static final int ELT_ID_TEXT_FORMATTING = 0x0A; + public static final int ELT_ID_PREDEFINED_SOUND = 0x0B; + public static final int ELT_ID_USER_DEFINED_SOUND = 0x0C; + public static final int ELT_ID_PREDEFINED_ANIMATION = 0x0D; + public static final int ELT_ID_LARGE_ANIMATION = 0x0E; + public static final int ELT_ID_SMALL_ANIMATION = 0x0F; + public static final int ELT_ID_LARGE_PICTURE = 0x10; + public static final int ELT_ID_SMALL_PICTURE = 0x11; + public static final int ELT_ID_VARIABLE_PICTURE = 0x12; + public static final int ELT_ID_USER_PROMPT_INDICATOR = 0x13; + public static final int ELT_ID_EXTENDED_OBJECT = 0x14; + public static final int ELT_ID_REUSED_EXTENDED_OBJECT = 0x15; + public static final int ELT_ID_COMPRESSION_CONTROL = 0x16; + public static final int ELT_ID_OBJECT_DISTR_INDICATOR = 0x17; + public static final int ELT_ID_STANDARD_WVG_OBJECT = 0x18; + public static final int ELT_ID_CHARACTER_SIZE_WVG_OBJECT = 0x19; + public static final int ELT_ID_EXTENDED_OBJECT_DATA_REQUEST_CMD = 0x1A; + public static final int ELT_ID_RFC_822_EMAIL_HEADER = 0x20; + public static final int ELT_ID_HYPERLINK_FORMAT_ELEMENT = 0x21; + public static final int ELT_ID_REPLY_ADDRESS_ELEMENT = 0x22; + public static final int ELT_ID_ENHANCED_VOICE_MAIL_INFORMATION = 0x23; + public static final int ELT_ID_NATIONAL_LANGUAGE_SINGLE_SHIFT = 0x24; + public static final int ELT_ID_NATIONAL_LANGUAGE_LOCKING_SHIFT = 0x25; + + public static final int PORT_WAP_PUSH = 2948; + public static final int PORT_WAP_WSP = 9200; + + public static class PortAddrs { + public int destPort; + public int origPort; + public boolean areEightBits; + } + + public static class ConcatRef { + public int refNumber; + public int seqNumber; + public int msgCount; + public boolean isEightBits; + } + + public static class SpecialSmsMsg { + public int msgIndType; + public int msgCount; + } + + /** + * A header element that is not explicitly parsed, meaning not + * PortAddrs or ConcatRef or SpecialSmsMsg. + */ + public static class MiscElt { + public int id; + public byte[] data; + } + + public PortAddrs portAddrs; + public ConcatRef concatRef; + public ArrayList<SpecialSmsMsg> specialSmsMsgList = new ArrayList<SpecialSmsMsg>(); + public ArrayList<MiscElt> miscEltList = new ArrayList<MiscElt>(); + + /** 7 bit national language locking shift table, or 0 for GSM default 7 bit alphabet. */ + public int languageTable; + + /** 7 bit national language single shift table, or 0 for GSM default 7 bit extension table. */ + public int languageShiftTable; + + public SmsHeader() {} + + /** + * Create structured SmsHeader object from serialized byte array representation. + * (see TS 23.040 9.2.3.24) + * @param data is user data header bytes + * @return SmsHeader object + */ + public static SmsHeader fromByteArray(byte[] data) { + ByteArrayInputStream inStream = new ByteArrayInputStream(data); + SmsHeader smsHeader = new SmsHeader(); + while (inStream.available() > 0) { + /** + * NOTE: as defined in the spec, ConcatRef and PortAddr + * fields should not reoccur, but if they do the last + * occurrence is to be used. Also, for ConcatRef + * elements, if the count is zero, sequence is zero, or + * sequence is larger than count, the entire element is to + * be ignored. + */ + int id = inStream.read(); + int length = inStream.read(); + ConcatRef concatRef; + PortAddrs portAddrs; + switch (id) { + case ELT_ID_CONCATENATED_8_BIT_REFERENCE: + concatRef = new ConcatRef(); + concatRef.refNumber = inStream.read(); + concatRef.msgCount = inStream.read(); + concatRef.seqNumber = inStream.read(); + concatRef.isEightBits = true; + if (concatRef.msgCount != 0 && concatRef.seqNumber != 0 && + concatRef.seqNumber <= concatRef.msgCount) { + smsHeader.concatRef = concatRef; + } + break; + case ELT_ID_CONCATENATED_16_BIT_REFERENCE: + concatRef = new ConcatRef(); + concatRef.refNumber = (inStream.read() << 8) | inStream.read(); + concatRef.msgCount = inStream.read(); + concatRef.seqNumber = inStream.read(); + concatRef.isEightBits = false; + if (concatRef.msgCount != 0 && concatRef.seqNumber != 0 && + concatRef.seqNumber <= concatRef.msgCount) { + smsHeader.concatRef = concatRef; + } + break; + case ELT_ID_APPLICATION_PORT_ADDRESSING_8_BIT: + portAddrs = new PortAddrs(); + portAddrs.destPort = inStream.read(); + portAddrs.origPort = inStream.read(); + portAddrs.areEightBits = true; + smsHeader.portAddrs = portAddrs; + break; + case ELT_ID_APPLICATION_PORT_ADDRESSING_16_BIT: + portAddrs = new PortAddrs(); + portAddrs.destPort = (inStream.read() << 8) | inStream.read(); + portAddrs.origPort = (inStream.read() << 8) | inStream.read(); + portAddrs.areEightBits = false; + smsHeader.portAddrs = portAddrs; + break; + case ELT_ID_NATIONAL_LANGUAGE_SINGLE_SHIFT: + smsHeader.languageShiftTable = inStream.read(); + break; + case ELT_ID_NATIONAL_LANGUAGE_LOCKING_SHIFT: + smsHeader.languageTable = inStream.read(); + break; + case ELT_ID_SPECIAL_SMS_MESSAGE_INDICATION: + SpecialSmsMsg specialSmsMsg = new SpecialSmsMsg(); + specialSmsMsg.msgIndType = inStream.read(); + specialSmsMsg.msgCount = inStream.read(); + smsHeader.specialSmsMsgList.add(specialSmsMsg); + break; + default: + MiscElt miscElt = new MiscElt(); + miscElt.id = id; + miscElt.data = new byte[length]; + inStream.read(miscElt.data, 0, length); + smsHeader.miscEltList.add(miscElt); + } + } + return smsHeader; + } + + /** + * Create serialized byte array representation from structured SmsHeader object. + * (see TS 23.040 9.2.3.24) + * @return Byte array representing the SmsHeader + */ + public static byte[] toByteArray(SmsHeader smsHeader) { + if ((smsHeader.portAddrs == null) && + (smsHeader.concatRef == null) && + (smsHeader.specialSmsMsgList.isEmpty()) && + (smsHeader.miscEltList.isEmpty()) && + (smsHeader.languageShiftTable == 0) && + (smsHeader.languageTable == 0)) { + return null; + } + + ByteArrayOutputStream outStream = + new ByteArrayOutputStream(SmsConstants.MAX_USER_DATA_BYTES); + ConcatRef concatRef = smsHeader.concatRef; + if (concatRef != null) { + if (concatRef.isEightBits) { + outStream.write(ELT_ID_CONCATENATED_8_BIT_REFERENCE); + outStream.write(3); + outStream.write(concatRef.refNumber); + } else { + outStream.write(ELT_ID_CONCATENATED_16_BIT_REFERENCE); + outStream.write(4); + outStream.write(concatRef.refNumber >>> 8); + outStream.write(concatRef.refNumber & 0x00FF); + } + outStream.write(concatRef.msgCount); + outStream.write(concatRef.seqNumber); + } + PortAddrs portAddrs = smsHeader.portAddrs; + if (portAddrs != null) { + if (portAddrs.areEightBits) { + outStream.write(ELT_ID_APPLICATION_PORT_ADDRESSING_8_BIT); + outStream.write(2); + outStream.write(portAddrs.destPort); + outStream.write(portAddrs.origPort); + } else { + outStream.write(ELT_ID_APPLICATION_PORT_ADDRESSING_16_BIT); + outStream.write(4); + outStream.write(portAddrs.destPort >>> 8); + outStream.write(portAddrs.destPort & 0x00FF); + outStream.write(portAddrs.origPort >>> 8); + outStream.write(portAddrs.origPort & 0x00FF); + } + } + if (smsHeader.languageShiftTable != 0) { + outStream.write(ELT_ID_NATIONAL_LANGUAGE_SINGLE_SHIFT); + outStream.write(1); + outStream.write(smsHeader.languageShiftTable); + } + if (smsHeader.languageTable != 0) { + outStream.write(ELT_ID_NATIONAL_LANGUAGE_LOCKING_SHIFT); + outStream.write(1); + outStream.write(smsHeader.languageTable); + } + for (SpecialSmsMsg specialSmsMsg : smsHeader.specialSmsMsgList) { + outStream.write(ELT_ID_SPECIAL_SMS_MESSAGE_INDICATION); + outStream.write(2); + outStream.write(specialSmsMsg.msgIndType & 0xFF); + outStream.write(specialSmsMsg.msgCount & 0xFF); + } + for (MiscElt miscElt : smsHeader.miscEltList) { + outStream.write(miscElt.id); + outStream.write(miscElt.data.length); + outStream.write(miscElt.data, 0, miscElt.data.length); + } + return outStream.toByteArray(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("UserDataHeader "); + builder.append("{ ConcatRef "); + if (concatRef == null) { + builder.append("unset"); + } else { + builder.append("{ refNumber=" + concatRef.refNumber); + builder.append(", msgCount=" + concatRef.msgCount); + builder.append(", seqNumber=" + concatRef.seqNumber); + builder.append(", isEightBits=" + concatRef.isEightBits); + builder.append(" }"); + } + builder.append(", PortAddrs "); + if (portAddrs == null) { + builder.append("unset"); + } else { + builder.append("{ destPort=" + portAddrs.destPort); + builder.append(", origPort=" + portAddrs.origPort); + builder.append(", areEightBits=" + portAddrs.areEightBits); + builder.append(" }"); + } + if (languageShiftTable != 0) { + builder.append(", languageShiftTable=" + languageShiftTable); + } + if (languageTable != 0) { + builder.append(", languageTable=" + languageTable); + } + for (SpecialSmsMsg specialSmsMsg : specialSmsMsgList) { + builder.append(", SpecialSmsMsg "); + builder.append("{ msgIndType=" + specialSmsMsg.msgIndType); + builder.append(", msgCount=" + specialSmsMsg.msgCount); + builder.append(" }"); + } + for (MiscElt miscElt : miscEltList) { + builder.append(", MiscElt "); + builder.append("{ id=" + miscElt.id); + builder.append(", length=" + miscElt.data.length); + builder.append(", data=" + HexDump.toHexString(miscElt.data)); + builder.append(" }"); + } + builder.append(" }"); + return builder.toString(); + } + +} diff --git a/telephony/java/com/android/internal/telephony/SmsMessageBase.java b/telephony/java/com/android/internal/telephony/SmsMessageBase.java new file mode 100644 index 000000000000..e5821dcd94a5 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/SmsMessageBase.java @@ -0,0 +1,433 @@ +/* + * Copyright (C) 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 com.android.internal.telephony; + +import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails; +import com.android.internal.telephony.SmsConstants; +import com.android.internal.telephony.SmsHeader; +import java.text.BreakIterator; +import java.util.Arrays; + +import android.provider.Telephony; +import android.telephony.SmsMessage; +import android.text.Emoji; + +/** + * Base class declaring the specific methods and members for SmsMessage. + * {@hide} + */ +public abstract class SmsMessageBase { + /** {@hide} The address of the SMSC. May be null */ + protected String mScAddress; + + /** {@hide} The address of the sender */ + protected SmsAddress mOriginatingAddress; + + /** {@hide} The message body as a string. May be null if the message isn't text */ + protected String mMessageBody; + + /** {@hide} */ + protected String mPseudoSubject; + + /** {@hide} Non-null if this is an email gateway message */ + protected String mEmailFrom; + + /** {@hide} Non-null if this is an email gateway message */ + protected String mEmailBody; + + /** {@hide} */ + protected boolean mIsEmail; + + /** {@hide} Time when SC (service centre) received the message */ + protected long mScTimeMillis; + + /** {@hide} The raw PDU of the message */ + protected byte[] mPdu; + + /** {@hide} The raw bytes for the user data section of the message */ + protected byte[] mUserData; + + /** {@hide} */ + protected SmsHeader mUserDataHeader; + + // "Message Waiting Indication Group" + // 23.038 Section 4 + /** {@hide} */ + protected boolean mIsMwi; + + /** {@hide} */ + protected boolean mMwiSense; + + /** {@hide} */ + protected boolean mMwiDontStore; + + /** + * Indicates status for messages stored on the ICC. + */ + protected int mStatusOnIcc = -1; + + /** + * Record index of message in the EF. + */ + protected int mIndexOnIcc = -1; + + /** TP-Message-Reference - Message Reference of sent message. @hide */ + public int mMessageRef; + + // TODO(): This class is duplicated in SmsMessage.java. Refactor accordingly. + public static abstract class SubmitPduBase { + public byte[] encodedScAddress; // Null if not applicable. + public byte[] encodedMessage; + + @Override + public String toString() { + return "SubmitPdu: encodedScAddress = " + + Arrays.toString(encodedScAddress) + + ", encodedMessage = " + + Arrays.toString(encodedMessage); + } + } + + /** + * Returns the address of the SMS service center that relayed this message + * or null if there is none. + */ + public String getServiceCenterAddress() { + return mScAddress; + } + + /** + * Returns the originating address (sender) of this SMS message in String + * form or null if unavailable + */ + public String getOriginatingAddress() { + if (mOriginatingAddress == null) { + return null; + } + + return mOriginatingAddress.getAddressString(); + } + + /** + * Returns the originating address, or email from address if this message + * was from an email gateway. Returns null if originating address + * unavailable. + */ + public String getDisplayOriginatingAddress() { + if (mIsEmail) { + return mEmailFrom; + } else { + return getOriginatingAddress(); + } + } + + /** + * Returns the message body as a String, if it exists and is text based. + * @return message body is there is one, otherwise null + */ + public String getMessageBody() { + return mMessageBody; + } + + /** + * Returns the class of this message. + */ + public abstract SmsConstants.MessageClass getMessageClass(); + + /** + * Returns the message body, or email message body if this message was from + * an email gateway. Returns null if message body unavailable. + */ + public String getDisplayMessageBody() { + if (mIsEmail) { + return mEmailBody; + } else { + return getMessageBody(); + } + } + + /** + * Unofficial convention of a subject line enclosed in parens empty string + * if not present + */ + public String getPseudoSubject() { + return mPseudoSubject == null ? "" : mPseudoSubject; + } + + /** + * Returns the service centre timestamp in currentTimeMillis() format + */ + public long getTimestampMillis() { + return mScTimeMillis; + } + + /** + * Returns true if message is an email. + * + * @return true if this message came through an email gateway and email + * sender / subject / parsed body are available + */ + public boolean isEmail() { + return mIsEmail; + } + + /** + * @return if isEmail() is true, body of the email sent through the gateway. + * null otherwise + */ + public String getEmailBody() { + return mEmailBody; + } + + /** + * @return if isEmail() is true, email from address of email sent through + * the gateway. null otherwise + */ + public String getEmailFrom() { + return mEmailFrom; + } + + /** + * Get protocol identifier. + */ + public abstract int getProtocolIdentifier(); + + /** + * See TS 23.040 9.2.3.9 returns true if this is a "replace short message" + * SMS + */ + public abstract boolean isReplace(); + + /** + * Returns true for CPHS MWI toggle message. + * + * @return true if this is a CPHS MWI toggle message See CPHS 4.2 section + * B.4.2 + */ + public abstract boolean isCphsMwiMessage(); + + /** + * returns true if this message is a CPHS voicemail / message waiting + * indicator (MWI) clear message + */ + public abstract boolean isMWIClearMessage(); + + /** + * returns true if this message is a CPHS voicemail / message waiting + * indicator (MWI) set message + */ + public abstract boolean isMWISetMessage(); + + /** + * returns true if this message is a "Message Waiting Indication Group: + * Discard Message" notification and should not be stored. + */ + public abstract boolean isMwiDontStore(); + + /** + * returns the user data section minus the user data header if one was + * present. + */ + public byte[] getUserData() { + return mUserData; + } + + /** + * Returns an object representing the user data header + * + * {@hide} + */ + public SmsHeader getUserDataHeader() { + return mUserDataHeader; + } + + /** + * TODO(cleanup): The term PDU is used in a seemingly non-unique + * manner -- for example, what is the difference between this byte + * array and the contents of SubmitPdu objects. Maybe a more + * illustrative term would be appropriate. + */ + + /** + * Returns the raw PDU for the message. + */ + public byte[] getPdu() { + return mPdu; + } + + /** + * For an SMS-STATUS-REPORT message, this returns the status field from + * the status report. This field indicates the status of a previously + * submitted SMS, if requested. See TS 23.040, 9.2.3.15 TP-Status for a + * description of values. + * + * @return 0 indicates the previously sent message was received. + * See TS 23.040, 9.9.2.3.15 for a description of other possible + * values. + */ + public abstract int getStatus(); + + /** + * Return true iff the message is a SMS-STATUS-REPORT message. + */ + public abstract boolean isStatusReportMessage(); + + /** + * Returns true iff the <code>TP-Reply-Path</code> bit is set in + * this message. + */ + public abstract boolean isReplyPathPresent(); + + /** + * Returns the status of the message on the ICC (read, unread, sent, unsent). + * + * @return the status of the message on the ICC. These are: + * SmsManager.STATUS_ON_ICC_FREE + * SmsManager.STATUS_ON_ICC_READ + * SmsManager.STATUS_ON_ICC_UNREAD + * SmsManager.STATUS_ON_ICC_SEND + * SmsManager.STATUS_ON_ICC_UNSENT + */ + public int getStatusOnIcc() { + return mStatusOnIcc; + } + + /** + * Returns the record index of the message on the ICC (1-based index). + * @return the record index of the message on the ICC, or -1 if this + * SmsMessage was not created from a ICC SMS EF record. + */ + public int getIndexOnIcc() { + return mIndexOnIcc; + } + + protected void parseMessageBody() { + // originatingAddress could be null if this message is from a status + // report. + if (mOriginatingAddress != null && mOriginatingAddress.couldBeEmailGateway()) { + extractEmailAddressFromMessageBody(); + } + } + + /** + * Try to parse this message as an email gateway message + * There are two ways specified in TS 23.040 Section 3.8 : + * - SMS message "may have its TP-PID set for Internet electronic mail - MT + * SMS format: [<from-address><space>]<message> - "Depending on the + * nature of the gateway, the destination/origination address is either + * derived from the content of the SMS TP-OA or TP-DA field, or the + * TP-OA/TP-DA field contains a generic gateway address and the to/from + * address is added at the beginning as shown above." (which is supported here) + * - Multiple addresses separated by commas, no spaces, Subject field delimited + * by '()' or '##' and '#' Section 9.2.3.24.11 (which are NOT supported here) + */ + protected void extractEmailAddressFromMessageBody() { + + /* Some carriers may use " /" delimiter as below + * + * 1. [x@y][ ]/[subject][ ]/[body] + * -or- + * 2. [x@y][ ]/[body] + */ + String[] parts = mMessageBody.split("( /)|( )", 2); + if (parts.length < 2) return; + mEmailFrom = parts[0]; + mEmailBody = parts[1]; + mIsEmail = Telephony.Mms.isEmailAddress(mEmailFrom); + } + + /** + * Find the next position to start a new fragment of a multipart SMS. + * + * @param currentPosition current start position of the fragment + * @param byteLimit maximum number of bytes in the fragment + * @param msgBody text of the SMS in UTF-16 encoding + * @return the position to start the next fragment + */ + public static int findNextUnicodePosition( + int currentPosition, int byteLimit, CharSequence msgBody) { + int nextPos = Math.min(currentPosition + byteLimit / 2, msgBody.length()); + // Check whether the fragment ends in a character boundary. Some characters take 4-bytes + // in UTF-16 encoding. Many carriers cannot handle + // a fragment correctly if it does not end at a character boundary. + if (nextPos < msgBody.length()) { + BreakIterator breakIterator = BreakIterator.getCharacterInstance(); + breakIterator.setText(msgBody.toString()); + if (!breakIterator.isBoundary(nextPos)) { + int breakPos = breakIterator.preceding(nextPos); + while (breakPos + 4 <= nextPos + && Emoji.isRegionalIndicatorSymbol( + Character.codePointAt(msgBody, breakPos)) + && Emoji.isRegionalIndicatorSymbol( + Character.codePointAt(msgBody, breakPos + 2))) { + // skip forward over flags (pairs of Regional Indicator Symbol) + breakPos += 4; + } + if (breakPos > currentPosition) { + nextPos = breakPos; + } else if (Character.isHighSurrogate(msgBody.charAt(nextPos - 1))) { + // no character boundary in this fragment, try to at least land on a code point + nextPos -= 1; + } + } + } + return nextPos; + } + + /** + * Calculate the TextEncodingDetails of a message encoded in Unicode. + */ + public static TextEncodingDetails calcUnicodeEncodingDetails(CharSequence msgBody) { + TextEncodingDetails ted = new TextEncodingDetails(); + int octets = msgBody.length() * 2; + ted.codeUnitSize = SmsConstants.ENCODING_16BIT; + ted.codeUnitCount = msgBody.length(); + if (octets > SmsConstants.MAX_USER_DATA_BYTES) { + // If EMS is not supported, break down EMS into single segment SMS + // and add page info " x/y". + // In the case of UCS2 encoding type, we need 8 bytes for this + // but we only have 6 bytes from UDH, so truncate the limit for + // each segment by 2 bytes (1 char). + int maxUserDataBytesWithHeader = SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER; + if (!SmsMessage.hasEmsSupport()) { + // make sure total number of segments is less than 10 + if (octets <= 9 * (maxUserDataBytesWithHeader - 2)) { + maxUserDataBytesWithHeader -= 2; + } + } + + int pos = 0; // Index in code units. + int msgCount = 0; + while (pos < msgBody.length()) { + int nextPos = findNextUnicodePosition(pos, maxUserDataBytesWithHeader, + msgBody); + if (nextPos == msgBody.length()) { + ted.codeUnitsRemaining = pos + maxUserDataBytesWithHeader / 2 - + msgBody.length(); + } + pos = nextPos; + msgCount++; + } + ted.msgCount = msgCount; + } else { + ted.msgCount = 1; + ted.codeUnitsRemaining = (SmsConstants.MAX_USER_DATA_BYTES - octets) / 2; + } + + return ted; + } +} diff --git a/telephony/java/com/android/internal/telephony/cdma/BearerData.java b/telephony/java/com/android/internal/telephony/cdma/BearerData.java new file mode 100644 index 000000000000..1de72dbf2829 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/cdma/BearerData.java @@ -0,0 +1,2000 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma.sms; + +import android.content.res.Resources; +import android.telephony.SmsCbCmasInfo; +import android.telephony.cdma.CdmaSmsCbProgramData; +import android.telephony.cdma.CdmaSmsCbProgramResults; +import android.text.format.Time; +import android.telephony.Rlog; + +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails; +import com.android.internal.telephony.SmsConstants; +import com.android.internal.telephony.SmsHeader; +import com.android.internal.telephony.SmsMessageBase; +import com.android.internal.telephony.uicc.IccUtils; +import com.android.internal.util.BitwiseInputStream; +import com.android.internal.util.BitwiseOutputStream; + +import java.util.ArrayList; +import java.util.TimeZone; + +/** + * An object to encode and decode CDMA SMS bearer data. + */ +public final class BearerData { + private final static String LOG_TAG = "BearerData"; + + /** + * Bearer Data Subparameter Identifiers + * (See 3GPP2 C.S0015-B, v2.0, table 4.5-1) + * NOTE: Commented subparameter types are not implemented. + */ + private final static byte SUBPARAM_MESSAGE_IDENTIFIER = 0x00; + private final static byte SUBPARAM_USER_DATA = 0x01; + private final static byte SUBPARAM_USER_RESPONSE_CODE = 0x02; + private final static byte SUBPARAM_MESSAGE_CENTER_TIME_STAMP = 0x03; + private final static byte SUBPARAM_VALIDITY_PERIOD_ABSOLUTE = 0x04; + private final static byte SUBPARAM_VALIDITY_PERIOD_RELATIVE = 0x05; + private final static byte SUBPARAM_DEFERRED_DELIVERY_TIME_ABSOLUTE = 0x06; + private final static byte SUBPARAM_DEFERRED_DELIVERY_TIME_RELATIVE = 0x07; + private final static byte SUBPARAM_PRIORITY_INDICATOR = 0x08; + private final static byte SUBPARAM_PRIVACY_INDICATOR = 0x09; + private final static byte SUBPARAM_REPLY_OPTION = 0x0A; + private final static byte SUBPARAM_NUMBER_OF_MESSAGES = 0x0B; + private final static byte SUBPARAM_ALERT_ON_MESSAGE_DELIVERY = 0x0C; + private final static byte SUBPARAM_LANGUAGE_INDICATOR = 0x0D; + private final static byte SUBPARAM_CALLBACK_NUMBER = 0x0E; + private final static byte SUBPARAM_MESSAGE_DISPLAY_MODE = 0x0F; + //private final static byte SUBPARAM_MULTIPLE_ENCODING_USER_DATA = 0x10; + private final static byte SUBPARAM_MESSAGE_DEPOSIT_INDEX = 0x11; + private final static byte SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA = 0x12; + private final static byte SUBPARAM_SERVICE_CATEGORY_PROGRAM_RESULTS = 0x13; + private final static byte SUBPARAM_MESSAGE_STATUS = 0x14; + //private final static byte SUBPARAM_TP_FAILURE_CAUSE = 0x15; + //private final static byte SUBPARAM_ENHANCED_VMN = 0x16; + //private final static byte SUBPARAM_ENHANCED_VMN_ACK = 0x17; + + // All other values after this are reserved. + private final static byte SUBPARAM_ID_LAST_DEFINED = 0x17; + + /** + * Supported message types for CDMA SMS messages + * (See 3GPP2 C.S0015-B, v2.0, table 4.5.1-1) + */ + public static final int MESSAGE_TYPE_DELIVER = 0x01; + public static final int MESSAGE_TYPE_SUBMIT = 0x02; + public static final int MESSAGE_TYPE_CANCELLATION = 0x03; + public static final int MESSAGE_TYPE_DELIVERY_ACK = 0x04; + public static final int MESSAGE_TYPE_USER_ACK = 0x05; + public static final int MESSAGE_TYPE_READ_ACK = 0x06; + public static final int MESSAGE_TYPE_DELIVER_REPORT = 0x07; + public static final int MESSAGE_TYPE_SUBMIT_REPORT = 0x08; + + public int messageType; + + /** + * 16-bit value indicating the message ID, which increments modulo 65536. + * (Special rules apply for WAP-messages.) + * (See 3GPP2 C.S0015-B, v2, 4.5.1) + */ + public int messageId; + + /** + * Supported priority modes for CDMA SMS messages + * (See 3GPP2 C.S0015-B, v2.0, table 4.5.9-1) + */ + public static final int PRIORITY_NORMAL = 0x0; + public static final int PRIORITY_INTERACTIVE = 0x1; + public static final int PRIORITY_URGENT = 0x2; + public static final int PRIORITY_EMERGENCY = 0x3; + + public boolean priorityIndicatorSet = false; + public int priority = PRIORITY_NORMAL; + + /** + * Supported privacy modes for CDMA SMS messages + * (See 3GPP2 C.S0015-B, v2.0, table 4.5.10-1) + */ + public static final int PRIVACY_NOT_RESTRICTED = 0x0; + public static final int PRIVACY_RESTRICTED = 0x1; + public static final int PRIVACY_CONFIDENTIAL = 0x2; + public static final int PRIVACY_SECRET = 0x3; + + public boolean privacyIndicatorSet = false; + public int privacy = PRIVACY_NOT_RESTRICTED; + + /** + * Supported alert priority modes for CDMA SMS messages + * (See 3GPP2 C.S0015-B, v2.0, table 4.5.13-1) + */ + public static final int ALERT_DEFAULT = 0x0; + public static final int ALERT_LOW_PRIO = 0x1; + public static final int ALERT_MEDIUM_PRIO = 0x2; + public static final int ALERT_HIGH_PRIO = 0x3; + + public boolean alertIndicatorSet = false; + public int alert = ALERT_DEFAULT; + + /** + * Supported display modes for CDMA SMS messages. Display mode is + * a 2-bit value used to indicate to the mobile station when to + * display the received message. (See 3GPP2 C.S0015-B, v2, + * 4.5.16) + */ + public static final int DISPLAY_MODE_IMMEDIATE = 0x0; + public static final int DISPLAY_MODE_DEFAULT = 0x1; + public static final int DISPLAY_MODE_USER = 0x2; + + public boolean displayModeSet = false; + public int displayMode = DISPLAY_MODE_DEFAULT; + + /** + * Language Indicator values. NOTE: the spec (3GPP2 C.S0015-B, + * v2, 4.5.14) is ambiguous as to the meaning of this field, as it + * refers to C.R1001-D but that reference has been crossed out. + * It would seem reasonable to assume the values from C.R1001-F + * (table 9.2-1) are to be used instead. + */ + public static final int LANGUAGE_UNKNOWN = 0x00; + public static final int LANGUAGE_ENGLISH = 0x01; + public static final int LANGUAGE_FRENCH = 0x02; + public static final int LANGUAGE_SPANISH = 0x03; + public static final int LANGUAGE_JAPANESE = 0x04; + public static final int LANGUAGE_KOREAN = 0x05; + public static final int LANGUAGE_CHINESE = 0x06; + public static final int LANGUAGE_HEBREW = 0x07; + + public boolean languageIndicatorSet = false; + public int language = LANGUAGE_UNKNOWN; + + /** + * SMS Message Status Codes. The first component of the Message + * status indicates if an error has occurred and whether the error + * is considered permanent or temporary. The second component of + * the Message status indicates the cause of the error (if any). + * (See 3GPP2 C.S0015-B, v2.0, 4.5.21) + */ + /* no-error codes */ + public static final int ERROR_NONE = 0x00; + public static final int STATUS_ACCEPTED = 0x00; + public static final int STATUS_DEPOSITED_TO_INTERNET = 0x01; + public static final int STATUS_DELIVERED = 0x02; + public static final int STATUS_CANCELLED = 0x03; + /* temporary-error and permanent-error codes */ + public static final int ERROR_TEMPORARY = 0x02; + public static final int STATUS_NETWORK_CONGESTION = 0x04; + public static final int STATUS_NETWORK_ERROR = 0x05; + public static final int STATUS_UNKNOWN_ERROR = 0x1F; + /* permanent-error codes */ + public static final int ERROR_PERMANENT = 0x03; + public static final int STATUS_CANCEL_FAILED = 0x06; + public static final int STATUS_BLOCKED_DESTINATION = 0x07; + public static final int STATUS_TEXT_TOO_LONG = 0x08; + public static final int STATUS_DUPLICATE_MESSAGE = 0x09; + public static final int STATUS_INVALID_DESTINATION = 0x0A; + public static final int STATUS_MESSAGE_EXPIRED = 0x0D; + /* undefined-status codes */ + public static final int ERROR_UNDEFINED = 0xFF; + public static final int STATUS_UNDEFINED = 0xFF; + + public boolean messageStatusSet = false; + public int errorClass = ERROR_UNDEFINED; + public int messageStatus = STATUS_UNDEFINED; + + /** + * 1-bit value that indicates whether a User Data Header (UDH) is present. + * (See 3GPP2 C.S0015-B, v2, 4.5.1) + * + * NOTE: during encoding, this value will be set based on the + * presence of a UDH in the structured data, any existing setting + * will be overwritten. + */ + public boolean hasUserDataHeader; + + /** + * provides the information for the user data + * (e.g. padding bits, user data, user data header, etc) + * (See 3GPP2 C.S.0015-B, v2, 4.5.2) + */ + public UserData userData; + + /** + * The User Response Code subparameter is used in the SMS User + * Acknowledgment Message to respond to previously received short + * messages. This message center-specific element carries the + * identifier of a predefined response. (See 3GPP2 C.S.0015-B, v2, + * 4.5.3) + */ + public boolean userResponseCodeSet = false; + public int userResponseCode; + + /** + * 6-byte-field, see 3GPP2 C.S0015-B, v2, 4.5.4 + */ + public static class TimeStamp extends Time { + + public TimeStamp() { + super(TimeZone.getDefault().getID()); // 3GPP2 timestamps use the local timezone + } + + public static TimeStamp fromByteArray(byte[] data) { + TimeStamp ts = new TimeStamp(); + // C.S0015-B v2.0, 4.5.4: range is 1996-2095 + int year = IccUtils.cdmaBcdByteToInt(data[0]); + if (year > 99 || year < 0) return null; + ts.year = year >= 96 ? year + 1900 : year + 2000; + int month = IccUtils.cdmaBcdByteToInt(data[1]); + if (month < 1 || month > 12) return null; + ts.month = month - 1; + int day = IccUtils.cdmaBcdByteToInt(data[2]); + if (day < 1 || day > 31) return null; + ts.monthDay = day; + int hour = IccUtils.cdmaBcdByteToInt(data[3]); + if (hour < 0 || hour > 23) return null; + ts.hour = hour; + int minute = IccUtils.cdmaBcdByteToInt(data[4]); + if (minute < 0 || minute > 59) return null; + ts.minute = minute; + int second = IccUtils.cdmaBcdByteToInt(data[5]); + if (second < 0 || second > 59) return null; + ts.second = second; + return ts; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("TimeStamp "); + builder.append("{ year=" + year); + builder.append(", month=" + month); + builder.append(", day=" + monthDay); + builder.append(", hour=" + hour); + builder.append(", minute=" + minute); + builder.append(", second=" + second); + builder.append(" }"); + return builder.toString(); + } + } + + public TimeStamp msgCenterTimeStamp; + public TimeStamp validityPeriodAbsolute; + public TimeStamp deferredDeliveryTimeAbsolute; + + /** + * Relative time is specified as one byte, the value of which + * falls into a series of ranges, as specified below. The idea is + * that shorter time intervals allow greater precision -- the + * value means minutes from zero until the MINS_LIMIT (inclusive), + * upon which it means hours until the HOURS_LIMIT, and so + * forth. (See 3GPP2 C.S0015-B, v2, 4.5.6-1) + */ + public static final int RELATIVE_TIME_MINS_LIMIT = 143; + public static final int RELATIVE_TIME_HOURS_LIMIT = 167; + public static final int RELATIVE_TIME_DAYS_LIMIT = 196; + public static final int RELATIVE_TIME_WEEKS_LIMIT = 244; + public static final int RELATIVE_TIME_INDEFINITE = 245; + public static final int RELATIVE_TIME_NOW = 246; + public static final int RELATIVE_TIME_MOBILE_INACTIVE = 247; + public static final int RELATIVE_TIME_RESERVED = 248; + + public boolean validityPeriodRelativeSet; + public int validityPeriodRelative; + public boolean deferredDeliveryTimeRelativeSet; + public int deferredDeliveryTimeRelative; + + /** + * The Reply Option subparameter contains 1-bit values which + * indicate whether SMS acknowledgment is requested or not. (See + * 3GPP2 C.S0015-B, v2, 4.5.11) + */ + public boolean userAckReq; + public boolean deliveryAckReq; + public boolean readAckReq; + public boolean reportReq; + + /** + * The Number of Messages subparameter (8-bit value) is a decimal + * number in the 0 to 99 range representing the number of messages + * stored at the Voice Mail System. This element is used by the + * Voice Mail Notification service. (See 3GPP2 C.S0015-B, v2, + * 4.5.12) + */ + public int numberOfMessages; + + /** + * The Message Deposit Index subparameter is assigned by the + * message center as a unique index to the contents of the User + * Data subparameter in each message sent to a particular mobile + * station. The mobile station, when replying to a previously + * received short message which included a Message Deposit Index + * subparameter, may include the Message Deposit Index of the + * received message to indicate to the message center that the + * original contents of the message are to be included in the + * reply. (See 3GPP2 C.S0015-B, v2, 4.5.18) + */ + public int depositIndex; + + /** + * 4-bit or 8-bit value that indicates the number to be dialed in reply to a + * received SMS message. + * (See 3GPP2 C.S0015-B, v2, 4.5.15) + */ + public CdmaSmsAddress callbackNumber; + + /** + * CMAS warning notification information. + * @see #decodeCmasUserData(BearerData, int) + */ + public SmsCbCmasInfo cmasWarningInfo; + + /** + * The Service Category Program Data subparameter is used to enable and disable + * SMS broadcast service categories to display. If this subparameter is present, + * this field will contain a list of one or more + * {@link android.telephony.cdma.CdmaSmsCbProgramData} objects containing the + * operation(s) to perform. + */ + public ArrayList<CdmaSmsCbProgramData> serviceCategoryProgramData; + + /** + * The Service Category Program Results subparameter informs the message center + * of the results of a Service Category Program Data request. + */ + public ArrayList<CdmaSmsCbProgramResults> serviceCategoryProgramResults; + + + private static class CodingException extends Exception { + public CodingException(String s) { + super(s); + } + } + + /** + * Returns the language indicator as a two-character ISO 639 string. + * @return a two character ISO 639 language code + */ + public String getLanguage() { + return getLanguageCodeForValue(language); + } + + /** + * Converts a CDMA language indicator value to an ISO 639 two character language code. + * @param languageValue the CDMA language value to convert + * @return the two character ISO 639 language code for the specified value, or null if unknown + */ + private static String getLanguageCodeForValue(int languageValue) { + switch (languageValue) { + case LANGUAGE_ENGLISH: + return "en"; + + case LANGUAGE_FRENCH: + return "fr"; + + case LANGUAGE_SPANISH: + return "es"; + + case LANGUAGE_JAPANESE: + return "ja"; + + case LANGUAGE_KOREAN: + return "ko"; + + case LANGUAGE_CHINESE: + return "zh"; + + case LANGUAGE_HEBREW: + return "he"; + + default: + return null; + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("BearerData "); + builder.append("{ messageType=" + messageType); + builder.append(", messageId=" + messageId); + builder.append(", priority=" + (priorityIndicatorSet ? priority : "unset")); + builder.append(", privacy=" + (privacyIndicatorSet ? privacy : "unset")); + builder.append(", alert=" + (alertIndicatorSet ? alert : "unset")); + builder.append(", displayMode=" + (displayModeSet ? displayMode : "unset")); + builder.append(", language=" + (languageIndicatorSet ? language : "unset")); + builder.append(", errorClass=" + (messageStatusSet ? errorClass : "unset")); + builder.append(", msgStatus=" + (messageStatusSet ? messageStatus : "unset")); + builder.append(", msgCenterTimeStamp=" + + ((msgCenterTimeStamp != null) ? msgCenterTimeStamp : "unset")); + builder.append(", validityPeriodAbsolute=" + + ((validityPeriodAbsolute != null) ? validityPeriodAbsolute : "unset")); + builder.append(", validityPeriodRelative=" + + ((validityPeriodRelativeSet) ? validityPeriodRelative : "unset")); + builder.append(", deferredDeliveryTimeAbsolute=" + + ((deferredDeliveryTimeAbsolute != null) ? deferredDeliveryTimeAbsolute : "unset")); + builder.append(", deferredDeliveryTimeRelative=" + + ((deferredDeliveryTimeRelativeSet) ? deferredDeliveryTimeRelative : "unset")); + builder.append(", userAckReq=" + userAckReq); + builder.append(", deliveryAckReq=" + deliveryAckReq); + builder.append(", readAckReq=" + readAckReq); + builder.append(", reportReq=" + reportReq); + builder.append(", numberOfMessages=" + numberOfMessages); + builder.append(", callbackNumber=" + Rlog.pii(LOG_TAG, callbackNumber)); + builder.append(", depositIndex=" + depositIndex); + builder.append(", hasUserDataHeader=" + hasUserDataHeader); + builder.append(", userData=" + userData); + builder.append(" }"); + return builder.toString(); + } + + private static void encodeMessageId(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 3); + outStream.write(4, bData.messageType); + outStream.write(8, bData.messageId >> 8); + outStream.write(8, bData.messageId); + outStream.write(1, bData.hasUserDataHeader ? 1 : 0); + outStream.skip(3); + } + + private static int countAsciiSeptets(CharSequence msg, boolean force) { + int msgLen = msg.length(); + if (force) return msgLen; + for (int i = 0; i < msgLen; i++) { + if (UserData.charToAscii.get(msg.charAt(i), -1) == -1) { + return -1; + } + } + return msgLen; + } + + /** + * Calculate the message text encoding length, fragmentation, and other details. + * + * @param msg message text + * @param force7BitEncoding ignore (but still count) illegal characters if true + * @param isEntireMsg indicates if this is entire msg or a segment in multipart msg + * @return septet count, or -1 on failure + */ + public static TextEncodingDetails calcTextEncodingDetails(CharSequence msg, + boolean force7BitEncoding, boolean isEntireMsg) { + TextEncodingDetails ted; + int septets = countAsciiSeptets(msg, force7BitEncoding); + if (septets != -1 && septets <= SmsConstants.MAX_USER_DATA_SEPTETS) { + ted = new TextEncodingDetails(); + ted.msgCount = 1; + ted.codeUnitCount = septets; + ted.codeUnitsRemaining = SmsConstants.MAX_USER_DATA_SEPTETS - septets; + ted.codeUnitSize = SmsConstants.ENCODING_7BIT; + } else { + ted = com.android.internal.telephony.gsm.SmsMessage.calculateLength( + msg, force7BitEncoding); + if (ted.msgCount == 1 && ted.codeUnitSize == SmsConstants.ENCODING_7BIT && + isEntireMsg) { + // We don't support single-segment EMS, so calculate for 16-bit + // TODO: Consider supporting single-segment EMS + return SmsMessageBase.calcUnicodeEncodingDetails(msg); + } + } + return ted; + } + + private static byte[] encode7bitAscii(String msg, boolean force) + throws CodingException + { + try { + BitwiseOutputStream outStream = new BitwiseOutputStream(msg.length()); + int msgLen = msg.length(); + for (int i = 0; i < msgLen; i++) { + int charCode = UserData.charToAscii.get(msg.charAt(i), -1); + if (charCode == -1) { + if (force) { + outStream.write(7, UserData.UNENCODABLE_7_BIT_CHAR); + } else { + throw new CodingException("cannot ASCII encode (" + msg.charAt(i) + ")"); + } + } else { + outStream.write(7, charCode); + } + } + return outStream.toByteArray(); + } catch (BitwiseOutputStream.AccessException ex) { + throw new CodingException("7bit ASCII encode failed: " + ex); + } + } + + private static byte[] encodeUtf16(String msg) + throws CodingException + { + try { + return msg.getBytes("utf-16be"); + } catch (java.io.UnsupportedEncodingException ex) { + throw new CodingException("UTF-16 encode failed: " + ex); + } + } + + private static class Gsm7bitCodingResult { + int septets; + byte[] data; + } + + private static Gsm7bitCodingResult encode7bitGsm(String msg, int septetOffset, boolean force) + throws CodingException + { + try { + /* + * TODO(cleanup): It would be nice if GsmAlphabet provided + * an option to produce just the data without prepending + * the septet count, as this function is really just a + * wrapper to strip that off. Not to mention that the + * septet count is generally known prior to invocation of + * the encoder. Note that it cannot be derived from the + * resulting array length, since that cannot distinguish + * if the last contains either 1 or 8 valid bits. + * + * TODO(cleanup): The BitwiseXStreams could also be + * extended with byte-wise reversed endianness read/write + * routines to allow a corresponding implementation of + * stringToGsm7BitPacked, and potentially directly support + * access to the main bitwise stream from encode/decode. + */ + byte[] fullData = GsmAlphabet.stringToGsm7BitPacked(msg, septetOffset, !force, 0, 0); + Gsm7bitCodingResult result = new Gsm7bitCodingResult(); + result.data = new byte[fullData.length - 1]; + System.arraycopy(fullData, 1, result.data, 0, fullData.length - 1); + result.septets = fullData[0] & 0x00FF; + return result; + } catch (com.android.internal.telephony.EncodeException ex) { + throw new CodingException("7bit GSM encode failed: " + ex); + } + } + + private static void encode7bitEms(UserData uData, byte[] udhData, boolean force) + throws CodingException + { + int udhBytes = udhData.length + 1; // Add length octet. + int udhSeptets = ((udhBytes * 8) + 6) / 7; + Gsm7bitCodingResult gcr = encode7bitGsm(uData.payloadStr, udhSeptets, force); + uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET; + uData.msgEncodingSet = true; + uData.numFields = gcr.septets; + uData.payload = gcr.data; + uData.payload[0] = (byte)udhData.length; + System.arraycopy(udhData, 0, uData.payload, 1, udhData.length); + } + + private static void encode16bitEms(UserData uData, byte[] udhData) + throws CodingException + { + byte[] payload = encodeUtf16(uData.payloadStr); + int udhBytes = udhData.length + 1; // Add length octet. + int udhCodeUnits = (udhBytes + 1) / 2; + int payloadCodeUnits = payload.length / 2; + uData.msgEncoding = UserData.ENCODING_UNICODE_16; + uData.msgEncodingSet = true; + uData.numFields = udhCodeUnits + payloadCodeUnits; + uData.payload = new byte[uData.numFields * 2]; + uData.payload[0] = (byte)udhData.length; + System.arraycopy(udhData, 0, uData.payload, 1, udhData.length); + System.arraycopy(payload, 0, uData.payload, udhBytes, payload.length); + } + + private static void encodeEmsUserDataPayload(UserData uData) + throws CodingException + { + byte[] headerData = SmsHeader.toByteArray(uData.userDataHeader); + if (uData.msgEncodingSet) { + if (uData.msgEncoding == UserData.ENCODING_GSM_7BIT_ALPHABET) { + encode7bitEms(uData, headerData, true); + } else if (uData.msgEncoding == UserData.ENCODING_UNICODE_16) { + encode16bitEms(uData, headerData); + } else { + throw new CodingException("unsupported EMS user data encoding (" + + uData.msgEncoding + ")"); + } + } else { + try { + encode7bitEms(uData, headerData, false); + } catch (CodingException ex) { + encode16bitEms(uData, headerData); + } + } + } + + private static byte[] encodeShiftJis(String msg) throws CodingException { + try { + return msg.getBytes("Shift_JIS"); + } catch (java.io.UnsupportedEncodingException ex) { + throw new CodingException("Shift-JIS encode failed: " + ex); + } + } + + private static void encodeUserDataPayload(UserData uData) + throws CodingException + { + if ((uData.payloadStr == null) && (uData.msgEncoding != UserData.ENCODING_OCTET)) { + Rlog.e(LOG_TAG, "user data with null payloadStr"); + uData.payloadStr = ""; + } + + if (uData.userDataHeader != null) { + encodeEmsUserDataPayload(uData); + return; + } + + if (uData.msgEncodingSet) { + if (uData.msgEncoding == UserData.ENCODING_OCTET) { + if (uData.payload == null) { + Rlog.e(LOG_TAG, "user data with octet encoding but null payload"); + uData.payload = new byte[0]; + uData.numFields = 0; + } else { + uData.numFields = uData.payload.length; + } + } else { + if (uData.payloadStr == null) { + Rlog.e(LOG_TAG, "non-octet user data with null payloadStr"); + uData.payloadStr = ""; + } + if (uData.msgEncoding == UserData.ENCODING_GSM_7BIT_ALPHABET) { + Gsm7bitCodingResult gcr = encode7bitGsm(uData.payloadStr, 0, true); + uData.payload = gcr.data; + uData.numFields = gcr.septets; + } else if (uData.msgEncoding == UserData.ENCODING_7BIT_ASCII) { + uData.payload = encode7bitAscii(uData.payloadStr, true); + uData.numFields = uData.payloadStr.length(); + } else if (uData.msgEncoding == UserData.ENCODING_UNICODE_16) { + uData.payload = encodeUtf16(uData.payloadStr); + uData.numFields = uData.payloadStr.length(); + } else if (uData.msgEncoding == UserData.ENCODING_SHIFT_JIS) { + uData.payload = encodeShiftJis(uData.payloadStr); + uData.numFields = uData.payload.length; + } else { + throw new CodingException("unsupported user data encoding (" + + uData.msgEncoding + ")"); + } + } + } else { + try { + uData.payload = encode7bitAscii(uData.payloadStr, false); + uData.msgEncoding = UserData.ENCODING_7BIT_ASCII; + } catch (CodingException ex) { + uData.payload = encodeUtf16(uData.payloadStr); + uData.msgEncoding = UserData.ENCODING_UNICODE_16; + } + uData.numFields = uData.payloadStr.length(); + uData.msgEncodingSet = true; + } + } + + private static void encodeUserData(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException, CodingException + { + /* + * TODO(cleanup): Do we really need to set userData.payload as + * a side effect of encoding? If not, we could avoid data + * copies by passing outStream directly. + */ + encodeUserDataPayload(bData.userData); + bData.hasUserDataHeader = bData.userData.userDataHeader != null; + + if (bData.userData.payload.length > SmsConstants.MAX_USER_DATA_BYTES) { + throw new CodingException("encoded user data too large (" + + bData.userData.payload.length + + " > " + SmsConstants.MAX_USER_DATA_BYTES + " bytes)"); + } + + /* + * TODO(cleanup): figure out what the right answer is WRT paddingBits field + * + * userData.paddingBits = (userData.payload.length * 8) - (userData.numFields * 7); + * userData.paddingBits = 0; // XXX this seems better, but why? + * + */ + int dataBits = (bData.userData.payload.length * 8) - bData.userData.paddingBits; + int paramBits = dataBits + 13; + if ((bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) || + (bData.userData.msgEncoding == UserData.ENCODING_GSM_DCS)) { + paramBits += 8; + } + int paramBytes = (paramBits / 8) + ((paramBits % 8) > 0 ? 1 : 0); + int paddingBits = (paramBytes * 8) - paramBits; + outStream.write(8, paramBytes); + outStream.write(5, bData.userData.msgEncoding); + if ((bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) || + (bData.userData.msgEncoding == UserData.ENCODING_GSM_DCS)) { + outStream.write(8, bData.userData.msgType); + } + outStream.write(8, bData.userData.numFields); + outStream.writeByteArray(dataBits, bData.userData.payload); + if (paddingBits > 0) outStream.write(paddingBits, 0); + } + + private static void encodeReplyOption(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(1, bData.userAckReq ? 1 : 0); + outStream.write(1, bData.deliveryAckReq ? 1 : 0); + outStream.write(1, bData.readAckReq ? 1 : 0); + outStream.write(1, bData.reportReq ? 1 : 0); + outStream.write(4, 0); + } + + private static byte[] encodeDtmfSmsAddress(String address) { + int digits = address.length(); + int dataBits = digits * 4; + int dataBytes = (dataBits / 8); + dataBytes += (dataBits % 8) > 0 ? 1 : 0; + byte[] rawData = new byte[dataBytes]; + for (int i = 0; i < digits; i++) { + char c = address.charAt(i); + int val = 0; + if ((c >= '1') && (c <= '9')) val = c - '0'; + else if (c == '0') val = 10; + else if (c == '*') val = 11; + else if (c == '#') val = 12; + else return null; + rawData[i / 2] |= val << (4 - ((i % 2) * 4)); + } + return rawData; + } + + /* + * TODO(cleanup): CdmaSmsAddress encoding should make use of + * CdmaSmsAddress.parse provided that DTMF encoding is unified, + * and the difference in 4-bit vs. 8-bit is resolved. + */ + + private static void encodeCdmaSmsAddress(CdmaSmsAddress addr) throws CodingException { + if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) { + try { + addr.origBytes = addr.address.getBytes("US-ASCII"); + } catch (java.io.UnsupportedEncodingException ex) { + throw new CodingException("invalid SMS address, cannot convert to ASCII"); + } + } else { + addr.origBytes = encodeDtmfSmsAddress(addr.address); + } + } + + private static void encodeCallbackNumber(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException, CodingException + { + CdmaSmsAddress addr = bData.callbackNumber; + encodeCdmaSmsAddress(addr); + int paramBits = 9; + int dataBits = 0; + if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) { + paramBits += 7; + dataBits = addr.numberOfDigits * 8; + } else { + dataBits = addr.numberOfDigits * 4; + } + paramBits += dataBits; + int paramBytes = (paramBits / 8) + ((paramBits % 8) > 0 ? 1 : 0); + int paddingBits = (paramBytes * 8) - paramBits; + outStream.write(8, paramBytes); + outStream.write(1, addr.digitMode); + if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) { + outStream.write(3, addr.ton); + outStream.write(4, addr.numberPlan); + } + outStream.write(8, addr.numberOfDigits); + outStream.writeByteArray(dataBits, addr.origBytes); + if (paddingBits > 0) outStream.write(paddingBits, 0); + } + + private static void encodeMsgStatus(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(2, bData.errorClass); + outStream.write(6, bData.messageStatus); + } + + private static void encodeMsgCount(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(8, bData.numberOfMessages); + } + + private static void encodeValidityPeriodRel(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(8, bData.validityPeriodRelative); + } + + private static void encodePrivacyIndicator(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(2, bData.privacy); + outStream.skip(6); + } + + private static void encodeLanguageIndicator(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(8, bData.language); + } + + private static void encodeDisplayMode(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(2, bData.displayMode); + outStream.skip(6); + } + + private static void encodePriorityIndicator(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(2, bData.priority); + outStream.skip(6); + } + + private static void encodeMsgDeliveryAlert(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(2, bData.alert); + outStream.skip(6); + } + + private static void encodeScpResults(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + ArrayList<CdmaSmsCbProgramResults> results = bData.serviceCategoryProgramResults; + outStream.write(8, (results.size() * 4)); // 4 octets per program result + for (CdmaSmsCbProgramResults result : results) { + int category = result.getCategory(); + outStream.write(8, category >> 8); + outStream.write(8, category); + outStream.write(8, result.getLanguage()); + outStream.write(4, result.getCategoryResult()); + outStream.skip(4); + } + } + + /** + * Create serialized representation for BearerData object. + * (See 3GPP2 C.R1001-F, v1.0, section 4.5 for layout details) + * + * @param bData an instance of BearerData. + * + * @return byte array of raw encoded SMS bearer data. + */ + public static byte[] encode(BearerData bData) { + bData.hasUserDataHeader = ((bData.userData != null) && + (bData.userData.userDataHeader != null)); + try { + BitwiseOutputStream outStream = new BitwiseOutputStream(200); + outStream.write(8, SUBPARAM_MESSAGE_IDENTIFIER); + encodeMessageId(bData, outStream); + if (bData.userData != null) { + outStream.write(8, SUBPARAM_USER_DATA); + encodeUserData(bData, outStream); + } + if (bData.callbackNumber != null) { + outStream.write(8, SUBPARAM_CALLBACK_NUMBER); + encodeCallbackNumber(bData, outStream); + } + if (bData.userAckReq || bData.deliveryAckReq || bData.readAckReq || bData.reportReq) { + outStream.write(8, SUBPARAM_REPLY_OPTION); + encodeReplyOption(bData, outStream); + } + if (bData.numberOfMessages != 0) { + outStream.write(8, SUBPARAM_NUMBER_OF_MESSAGES); + encodeMsgCount(bData, outStream); + } + if (bData.validityPeriodRelativeSet) { + outStream.write(8, SUBPARAM_VALIDITY_PERIOD_RELATIVE); + encodeValidityPeriodRel(bData, outStream); + } + if (bData.privacyIndicatorSet) { + outStream.write(8, SUBPARAM_PRIVACY_INDICATOR); + encodePrivacyIndicator(bData, outStream); + } + if (bData.languageIndicatorSet) { + outStream.write(8, SUBPARAM_LANGUAGE_INDICATOR); + encodeLanguageIndicator(bData, outStream); + } + if (bData.displayModeSet) { + outStream.write(8, SUBPARAM_MESSAGE_DISPLAY_MODE); + encodeDisplayMode(bData, outStream); + } + if (bData.priorityIndicatorSet) { + outStream.write(8, SUBPARAM_PRIORITY_INDICATOR); + encodePriorityIndicator(bData, outStream); + } + if (bData.alertIndicatorSet) { + outStream.write(8, SUBPARAM_ALERT_ON_MESSAGE_DELIVERY); + encodeMsgDeliveryAlert(bData, outStream); + } + if (bData.messageStatusSet) { + outStream.write(8, SUBPARAM_MESSAGE_STATUS); + encodeMsgStatus(bData, outStream); + } + if (bData.serviceCategoryProgramResults != null) { + outStream.write(8, SUBPARAM_SERVICE_CATEGORY_PROGRAM_RESULTS); + encodeScpResults(bData, outStream); + } + return outStream.toByteArray(); + } catch (BitwiseOutputStream.AccessException ex) { + Rlog.e(LOG_TAG, "BearerData encode failed: " + ex); + } catch (CodingException ex) { + Rlog.e(LOG_TAG, "BearerData encode failed: " + ex); + } + return null; + } + + private static boolean decodeMessageId(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 3 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.messageType = inStream.read(4); + bData.messageId = inStream.read(8) << 8; + bData.messageId |= inStream.read(8); + bData.hasUserDataHeader = (inStream.read(1) == 1); + inStream.skip(3); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "MESSAGE_IDENTIFIER decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + return decodeSuccess; + } + + private static boolean decodeReserved( + BearerData bData, BitwiseInputStream inStream, int subparamId) + throws BitwiseInputStream.AccessException, CodingException + { + boolean decodeSuccess = false; + int subparamLen = inStream.read(8); // SUBPARAM_LEN + int paramBits = subparamLen * 8; + if (paramBits <= inStream.available()) { + decodeSuccess = true; + inStream.skip(paramBits); + } + Rlog.d(LOG_TAG, "RESERVED bearer data subparameter " + subparamId + " decode " + + (decodeSuccess ? "succeeded" : "failed") + " (param bits = " + paramBits + ")"); + if (!decodeSuccess) { + throw new CodingException("RESERVED bearer data subparameter " + subparamId + + " had invalid SUBPARAM_LEN " + subparamLen); + } + + return decodeSuccess; + } + + private static boolean decodeUserData(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException + { + int paramBits = inStream.read(8) * 8; + bData.userData = new UserData(); + bData.userData.msgEncoding = inStream.read(5); + bData.userData.msgEncodingSet = true; + bData.userData.msgType = 0; + int consumedBits = 5; + if ((bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) || + (bData.userData.msgEncoding == UserData.ENCODING_GSM_DCS)) { + bData.userData.msgType = inStream.read(8); + consumedBits += 8; + } + bData.userData.numFields = inStream.read(8); + consumedBits += 8; + int dataBits = paramBits - consumedBits; + bData.userData.payload = inStream.readByteArray(dataBits); + return true; + } + + private static String decodeUtf8(byte[] data, int offset, int numFields) + throws CodingException + { + return decodeCharset(data, offset, numFields, 1, "UTF-8"); + } + + private static String decodeUtf16(byte[] data, int offset, int numFields) + throws CodingException + { + // Subtract header and possible padding byte (at end) from num fields. + int padding = offset % 2; + numFields -= (offset + padding) / 2; + return decodeCharset(data, offset, numFields, 2, "utf-16be"); + } + + private static String decodeCharset(byte[] data, int offset, int numFields, int width, + String charset) throws CodingException + { + if (numFields < 0 || (numFields * width + offset) > data.length) { + // Try to decode the max number of characters in payload + int padding = offset % width; + int maxNumFields = (data.length - offset - padding) / width; + if (maxNumFields < 0) { + throw new CodingException(charset + " decode failed: offset out of range"); + } + Rlog.e(LOG_TAG, charset + " decode error: offset = " + offset + " numFields = " + + numFields + " data.length = " + data.length + " maxNumFields = " + + maxNumFields); + numFields = maxNumFields; + } + try { + return new String(data, offset, numFields * width, charset); + } catch (java.io.UnsupportedEncodingException ex) { + throw new CodingException(charset + " decode failed: " + ex); + } + } + + private static String decode7bitAscii(byte[] data, int offset, int numFields) + throws CodingException + { + try { + offset *= 8; + StringBuffer strBuf = new StringBuffer(numFields); + BitwiseInputStream inStream = new BitwiseInputStream(data); + int wantedBits = (offset * 8) + (numFields * 7); + if (inStream.available() < wantedBits) { + throw new CodingException("insufficient data (wanted " + wantedBits + + " bits, but only have " + inStream.available() + ")"); + } + inStream.skip(offset); + for (int i = 0; i < numFields; i++) { + int charCode = inStream.read(7); + if ((charCode >= UserData.ASCII_MAP_BASE_INDEX) && + (charCode <= UserData.ASCII_MAP_MAX_INDEX)) { + strBuf.append(UserData.ASCII_MAP[charCode - UserData.ASCII_MAP_BASE_INDEX]); + } else if (charCode == UserData.ASCII_NL_INDEX) { + strBuf.append('\n'); + } else if (charCode == UserData.ASCII_CR_INDEX) { + strBuf.append('\r'); + } else { + /* For other charCodes, they are unprintable, and so simply use SPACE. */ + strBuf.append(' '); + } + } + return strBuf.toString(); + } catch (BitwiseInputStream.AccessException ex) { + throw new CodingException("7bit ASCII decode failed: " + ex); + } + } + + private static String decode7bitGsm(byte[] data, int offset, int numFields) + throws CodingException + { + // Start reading from the next 7-bit aligned boundary after offset. + int offsetBits = offset * 8; + int offsetSeptets = (offsetBits + 6) / 7; + numFields -= offsetSeptets; + int paddingBits = (offsetSeptets * 7) - offsetBits; + String result = GsmAlphabet.gsm7BitPackedToString(data, offset, numFields, paddingBits, + 0, 0); + if (result == null) { + throw new CodingException("7bit GSM decoding failed"); + } + return result; + } + + private static String decodeLatin(byte[] data, int offset, int numFields) + throws CodingException + { + return decodeCharset(data, offset, numFields, 1, "ISO-8859-1"); + } + + private static String decodeShiftJis(byte[] data, int offset, int numFields) + throws CodingException + { + return decodeCharset(data, offset, numFields, 1, "Shift_JIS"); + } + + private static void decodeUserDataPayload(UserData userData, boolean hasUserDataHeader) + throws CodingException + { + int offset = 0; + if (hasUserDataHeader) { + int udhLen = userData.payload[0] & 0x00FF; + offset += udhLen + 1; + byte[] headerData = new byte[udhLen]; + System.arraycopy(userData.payload, 1, headerData, 0, udhLen); + userData.userDataHeader = SmsHeader.fromByteArray(headerData); + } + switch (userData.msgEncoding) { + case UserData.ENCODING_OCTET: + /* + * Octet decoding depends on the carrier service. + */ + boolean decodingtypeUTF8 = Resources.getSystem() + .getBoolean(com.android.internal.R.bool.config_sms_utf8_support); + + // Strip off any padding bytes, meaning any differences between the length of the + // array and the target length specified by numFields. This is to avoid any + // confusion by code elsewhere that only considers the payload array length. + byte[] payload = new byte[userData.numFields]; + int copyLen = userData.numFields < userData.payload.length + ? userData.numFields : userData.payload.length; + + System.arraycopy(userData.payload, 0, payload, 0, copyLen); + userData.payload = payload; + + if (!decodingtypeUTF8) { + // There are many devices in the market that send 8bit text sms (latin encoded) as + // octet encoded. + userData.payloadStr = decodeLatin(userData.payload, offset, userData.numFields); + } else { + userData.payloadStr = decodeUtf8(userData.payload, offset, userData.numFields); + } + break; + + case UserData.ENCODING_IA5: + case UserData.ENCODING_7BIT_ASCII: + userData.payloadStr = decode7bitAscii(userData.payload, offset, userData.numFields); + break; + case UserData.ENCODING_UNICODE_16: + userData.payloadStr = decodeUtf16(userData.payload, offset, userData.numFields); + break; + case UserData.ENCODING_GSM_7BIT_ALPHABET: + userData.payloadStr = decode7bitGsm(userData.payload, offset, userData.numFields); + break; + case UserData.ENCODING_LATIN: + userData.payloadStr = decodeLatin(userData.payload, offset, userData.numFields); + break; + case UserData.ENCODING_SHIFT_JIS: + userData.payloadStr = decodeShiftJis(userData.payload, offset, userData.numFields); + break; + default: + throw new CodingException("unsupported user data encoding (" + + userData.msgEncoding + ")"); + } + } + + /** + * IS-91 Voice Mail message decoding + * (See 3GPP2 C.S0015-A, Table 4.3.1.4.1-1) + * (For character encodings, see TIA/EIA/IS-91, Annex B) + * + * Protocol Summary: The user data payload may contain 3-14 + * characters. The first two characters are parsed as a number + * and indicate the number of voicemails. The third character is + * either a SPACE or '!' to indicate normal or urgent priority, + * respectively. Any following characters are treated as normal + * text user data payload. + * + * Note that the characters encoding is 6-bit packed. + */ + private static void decodeIs91VoicemailStatus(BearerData bData) + throws BitwiseInputStream.AccessException, CodingException + { + BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload); + int dataLen = inStream.available() / 6; // 6-bit packed character encoding. + int numFields = bData.userData.numFields; + if ((dataLen > 14) || (dataLen < 3) || (dataLen < numFields)) { + throw new CodingException("IS-91 voicemail status decoding failed"); + } + try { + StringBuffer strbuf = new StringBuffer(dataLen); + while (inStream.available() >= 6) { + strbuf.append(UserData.ASCII_MAP[inStream.read(6)]); + } + String data = strbuf.toString(); + bData.numberOfMessages = Integer.parseInt(data.substring(0, 2)); + char prioCode = data.charAt(2); + if (prioCode == ' ') { + bData.priority = PRIORITY_NORMAL; + } else if (prioCode == '!') { + bData.priority = PRIORITY_URGENT; + } else { + throw new CodingException("IS-91 voicemail status decoding failed: " + + "illegal priority setting (" + prioCode + ")"); + } + bData.priorityIndicatorSet = true; + bData.userData.payloadStr = data.substring(3, numFields - 3); + } catch (java.lang.NumberFormatException ex) { + throw new CodingException("IS-91 voicemail status decoding failed: " + ex); + } catch (java.lang.IndexOutOfBoundsException ex) { + throw new CodingException("IS-91 voicemail status decoding failed: " + ex); + } + } + + /** + * IS-91 Short Message decoding + * (See 3GPP2 C.S0015-A, Table 4.3.1.4.1-1) + * (For character encodings, see TIA/EIA/IS-91, Annex B) + * + * Protocol Summary: The user data payload may contain 1-14 + * characters, which are treated as normal text user data payload. + * Note that the characters encoding is 6-bit packed. + */ + private static void decodeIs91ShortMessage(BearerData bData) + throws BitwiseInputStream.AccessException, CodingException + { + BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload); + int dataLen = inStream.available() / 6; // 6-bit packed character encoding. + int numFields = bData.userData.numFields; + // dataLen may be > 14 characters due to octet padding + if ((numFields > 14) || (dataLen < numFields)) { + throw new CodingException("IS-91 short message decoding failed"); + } + StringBuffer strbuf = new StringBuffer(dataLen); + for (int i = 0; i < numFields; i++) { + strbuf.append(UserData.ASCII_MAP[inStream.read(6)]); + } + bData.userData.payloadStr = strbuf.toString(); + } + + /** + * IS-91 CLI message (callback number) decoding + * (See 3GPP2 C.S0015-A, Table 4.3.1.4.1-1) + * + * Protocol Summary: The data payload may contain 1-32 digits, + * encoded using standard 4-bit DTMF, which are treated as a + * callback number. + */ + private static void decodeIs91Cli(BearerData bData) throws CodingException { + BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload); + int dataLen = inStream.available() / 4; // 4-bit packed DTMF digit encoding. + int numFields = bData.userData.numFields; + if ((dataLen > 14) || (dataLen < 3) || (dataLen < numFields)) { + throw new CodingException("IS-91 voicemail status decoding failed"); + } + CdmaSmsAddress addr = new CdmaSmsAddress(); + addr.digitMode = CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF; + addr.origBytes = bData.userData.payload; + addr.numberOfDigits = (byte)numFields; + decodeSmsAddress(addr); + bData.callbackNumber = addr; + } + + private static void decodeIs91(BearerData bData) + throws BitwiseInputStream.AccessException, CodingException + { + switch (bData.userData.msgType) { + case UserData.IS91_MSG_TYPE_VOICEMAIL_STATUS: + decodeIs91VoicemailStatus(bData); + break; + case UserData.IS91_MSG_TYPE_CLI: + decodeIs91Cli(bData); + break; + case UserData.IS91_MSG_TYPE_SHORT_MESSAGE_FULL: + case UserData.IS91_MSG_TYPE_SHORT_MESSAGE: + decodeIs91ShortMessage(bData); + break; + default: + throw new CodingException("unsupported IS-91 message type (" + + bData.userData.msgType + ")"); + } + } + + private static boolean decodeReplyOption(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.userAckReq = (inStream.read(1) == 1); + bData.deliveryAckReq = (inStream.read(1) == 1); + bData.readAckReq = (inStream.read(1) == 1); + bData.reportReq = (inStream.read(1) == 1); + inStream.skip(4); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "REPLY_OPTION decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + return decodeSuccess; + } + + private static boolean decodeMsgCount(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.numberOfMessages = IccUtils.cdmaBcdByteToInt((byte)inStream.read(8)); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "NUMBER_OF_MESSAGES decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + return decodeSuccess; + } + + private static boolean decodeDepositIndex(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 2 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.depositIndex = (inStream.read(8) << 8) | inStream.read(8); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "MESSAGE_DEPOSIT_INDEX decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + return decodeSuccess; + } + + private static String decodeDtmfSmsAddress(byte[] rawData, int numFields) + throws CodingException + { + /* DTMF 4-bit digit encoding, defined in at + * 3GPP2 C.S005-D, v2.0, table 2.7.1.3.2.4-4 */ + StringBuffer strBuf = new StringBuffer(numFields); + for (int i = 0; i < numFields; i++) { + int val = 0x0F & (rawData[i / 2] >>> (4 - ((i % 2) * 4))); + if ((val >= 1) && (val <= 9)) strBuf.append(Integer.toString(val, 10)); + else if (val == 10) strBuf.append('0'); + else if (val == 11) strBuf.append('*'); + else if (val == 12) strBuf.append('#'); + else throw new CodingException("invalid SMS address DTMF code (" + val + ")"); + } + return strBuf.toString(); + } + + private static void decodeSmsAddress(CdmaSmsAddress addr) throws CodingException { + if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) { + try { + /* As specified in 3GPP2 C.S0015-B, v2, 4.5.15 -- actually + * just 7-bit ASCII encoding, with the MSB being zero. */ + addr.address = new String(addr.origBytes, 0, addr.origBytes.length, "US-ASCII"); + } catch (java.io.UnsupportedEncodingException ex) { + throw new CodingException("invalid SMS address ASCII code"); + } + } else { + addr.address = decodeDtmfSmsAddress(addr.origBytes, addr.numberOfDigits); + } + } + + private static boolean decodeCallbackNumber(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 1 * 8; //at least + int paramBits = inStream.read(8) * 8; + if (paramBits < EXPECTED_PARAM_SIZE) { + inStream.skip(paramBits); + return false; + } + CdmaSmsAddress addr = new CdmaSmsAddress(); + addr.digitMode = inStream.read(1); + byte fieldBits = 4; + byte consumedBits = 1; + if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) { + addr.ton = inStream.read(3); + addr.numberPlan = inStream.read(4); + fieldBits = 8; + consumedBits += 7; + } + addr.numberOfDigits = inStream.read(8); + consumedBits += 8; + int remainingBits = paramBits - consumedBits; + int dataBits = addr.numberOfDigits * fieldBits; + int paddingBits = remainingBits - dataBits; + if (remainingBits < dataBits) { + throw new CodingException("CALLBACK_NUMBER subparam encoding size error (" + + "remainingBits + " + remainingBits + ", dataBits + " + + dataBits + ", paddingBits + " + paddingBits + ")"); + } + addr.origBytes = inStream.readByteArray(dataBits); + inStream.skip(paddingBits); + decodeSmsAddress(addr); + bData.callbackNumber = addr; + return true; + } + + private static boolean decodeMsgStatus(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.errorClass = inStream.read(2); + bData.messageStatus = inStream.read(6); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "MESSAGE_STATUS decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.messageStatusSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodeMsgCenterTimeStamp(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 6 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.msgCenterTimeStamp = TimeStamp.fromByteArray(inStream.readByteArray(6 * 8)); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "MESSAGE_CENTER_TIME_STAMP decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + return decodeSuccess; + } + + private static boolean decodeValidityAbs(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 6 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.validityPeriodAbsolute = TimeStamp.fromByteArray(inStream.readByteArray(6 * 8)); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "VALIDITY_PERIOD_ABSOLUTE decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + return decodeSuccess; + } + + private static boolean decodeDeferredDeliveryAbs(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 6 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.deferredDeliveryTimeAbsolute = TimeStamp.fromByteArray( + inStream.readByteArray(6 * 8)); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "DEFERRED_DELIVERY_TIME_ABSOLUTE decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + return decodeSuccess; + } + + private static boolean decodeValidityRel(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.deferredDeliveryTimeRelative = inStream.read(8); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "VALIDITY_PERIOD_RELATIVE decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.deferredDeliveryTimeRelativeSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodeDeferredDeliveryRel(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.validityPeriodRelative = inStream.read(8); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "DEFERRED_DELIVERY_TIME_RELATIVE decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.validityPeriodRelativeSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodePrivacyIndicator(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.privacy = inStream.read(2); + inStream.skip(6); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "PRIVACY_INDICATOR decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.privacyIndicatorSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodeLanguageIndicator(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.language = inStream.read(8); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "LANGUAGE_INDICATOR decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.languageIndicatorSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodeDisplayMode(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.displayMode = inStream.read(2); + inStream.skip(6); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "DISPLAY_MODE decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.displayModeSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodePriorityIndicator(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.priority = inStream.read(2); + inStream.skip(6); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "PRIORITY_INDICATOR decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.priorityIndicatorSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodeMsgDeliveryAlert(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.alert = inStream.read(2); + inStream.skip(6); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "ALERT_ON_MESSAGE_DELIVERY decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.alertIndicatorSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodeUserResponseCode(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.userResponseCode = inStream.read(8); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "USER_RESPONSE_CODE decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.userResponseCodeSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodeServiceCategoryProgramData(BearerData bData, + BitwiseInputStream inStream) throws BitwiseInputStream.AccessException, CodingException + { + if (inStream.available() < 13) { + throw new CodingException("SERVICE_CATEGORY_PROGRAM_DATA decode failed: only " + + inStream.available() + " bits available"); + } + + int paramBits = inStream.read(8) * 8; + int msgEncoding = inStream.read(5); + paramBits -= 5; + + if (inStream.available() < paramBits) { + throw new CodingException("SERVICE_CATEGORY_PROGRAM_DATA decode failed: only " + + inStream.available() + " bits available (" + paramBits + " bits expected)"); + } + + ArrayList<CdmaSmsCbProgramData> programDataList = new ArrayList<CdmaSmsCbProgramData>(); + + final int CATEGORY_FIELD_MIN_SIZE = 6 * 8; + boolean decodeSuccess = false; + while (paramBits >= CATEGORY_FIELD_MIN_SIZE) { + int operation = inStream.read(4); + int category = (inStream.read(8) << 8) | inStream.read(8); + int language = inStream.read(8); + int maxMessages = inStream.read(8); + int alertOption = inStream.read(4); + int numFields = inStream.read(8); + paramBits -= CATEGORY_FIELD_MIN_SIZE; + + int textBits = getBitsForNumFields(msgEncoding, numFields); + if (paramBits < textBits) { + throw new CodingException("category name is " + textBits + " bits in length," + + " but there are only " + paramBits + " bits available"); + } + + UserData userData = new UserData(); + userData.msgEncoding = msgEncoding; + userData.msgEncodingSet = true; + userData.numFields = numFields; + userData.payload = inStream.readByteArray(textBits); + paramBits -= textBits; + + decodeUserDataPayload(userData, false); + String categoryName = userData.payloadStr; + CdmaSmsCbProgramData programData = new CdmaSmsCbProgramData(operation, category, + language, maxMessages, alertOption, categoryName); + programDataList.add(programData); + + decodeSuccess = true; + } + + if ((! decodeSuccess) || (paramBits > 0)) { + Rlog.d(LOG_TAG, "SERVICE_CATEGORY_PROGRAM_DATA decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ')'); + } + + inStream.skip(paramBits); + bData.serviceCategoryProgramData = programDataList; + return decodeSuccess; + } + + private static int serviceCategoryToCmasMessageClass(int serviceCategory) { + switch (serviceCategory) { + case SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT: + return SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT: + return SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT: + return SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY: + return SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE: + return SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST; + + default: + return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN; + } + } + + /** + * Calculates the number of bits to read for the specified number of encoded characters. + * @param msgEncoding the message encoding to use + * @param numFields the number of characters to read. For Shift-JIS and Korean encodings, + * this is the number of bytes to read. + * @return the number of bits to read from the stream + * @throws CodingException if the specified encoding is not supported + */ + private static int getBitsForNumFields(int msgEncoding, int numFields) + throws CodingException { + switch (msgEncoding) { + case UserData.ENCODING_OCTET: + case UserData.ENCODING_SHIFT_JIS: + case UserData.ENCODING_KOREAN: + case UserData.ENCODING_LATIN: + case UserData.ENCODING_LATIN_HEBREW: + return numFields * 8; + + case UserData.ENCODING_IA5: + case UserData.ENCODING_7BIT_ASCII: + case UserData.ENCODING_GSM_7BIT_ALPHABET: + return numFields * 7; + + case UserData.ENCODING_UNICODE_16: + return numFields * 16; + + default: + throw new CodingException("unsupported message encoding (" + msgEncoding + ')'); + } + } + + /** + * CMAS message decoding. + * (See TIA-1149-0-1, CMAS over CDMA) + * + * @param serviceCategory is the service category from the SMS envelope + */ + private static void decodeCmasUserData(BearerData bData, int serviceCategory) + throws BitwiseInputStream.AccessException, CodingException { + BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload); + if (inStream.available() < 8) { + throw new CodingException("emergency CB with no CMAE_protocol_version"); + } + int protocolVersion = inStream.read(8); + if (protocolVersion != 0) { + throw new CodingException("unsupported CMAE_protocol_version " + protocolVersion); + } + + int messageClass = serviceCategoryToCmasMessageClass(serviceCategory); + int category = SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN; + int responseType = SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN; + int severity = SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN; + int urgency = SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN; + int certainty = SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN; + + while (inStream.available() >= 16) { + int recordType = inStream.read(8); + int recordLen = inStream.read(8); + switch (recordType) { + case 0: // Type 0 elements (Alert text) + UserData alertUserData = new UserData(); + alertUserData.msgEncoding = inStream.read(5); + alertUserData.msgEncodingSet = true; + alertUserData.msgType = 0; + + int numFields; // number of chars to decode + switch (alertUserData.msgEncoding) { + case UserData.ENCODING_OCTET: + case UserData.ENCODING_LATIN: + numFields = recordLen - 1; // subtract 1 byte for encoding + break; + + case UserData.ENCODING_IA5: + case UserData.ENCODING_7BIT_ASCII: + case UserData.ENCODING_GSM_7BIT_ALPHABET: + numFields = ((recordLen * 8) - 5) / 7; // subtract 5 bits for encoding + break; + + case UserData.ENCODING_UNICODE_16: + numFields = (recordLen - 1) / 2; + break; + + default: + numFields = 0; // unsupported encoding + } + + alertUserData.numFields = numFields; + alertUserData.payload = inStream.readByteArray(recordLen * 8 - 5); + decodeUserDataPayload(alertUserData, false); + bData.userData = alertUserData; + break; + + case 1: // Type 1 elements + category = inStream.read(8); + responseType = inStream.read(8); + severity = inStream.read(4); + urgency = inStream.read(4); + certainty = inStream.read(4); + inStream.skip(recordLen * 8 - 28); + break; + + default: + Rlog.w(LOG_TAG, "skipping unsupported CMAS record type " + recordType); + inStream.skip(recordLen * 8); + break; + } + } + + bData.cmasWarningInfo = new SmsCbCmasInfo(messageClass, category, responseType, severity, + urgency, certainty); + } + + /** + * Create BearerData object from serialized representation. + * (See 3GPP2 C.R1001-F, v1.0, section 4.5 for layout details) + * + * @param smsData byte array of raw encoded SMS bearer data. + * @return an instance of BearerData. + */ + public static BearerData decode(byte[] smsData) { + return decode(smsData, 0); + } + + private static boolean isCmasAlertCategory(int category) { + return category >= SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT + && category <= SmsEnvelope.SERVICE_CATEGORY_CMAS_LAST_RESERVED_VALUE; + } + + /** + * Create BearerData object from serialized representation. + * (See 3GPP2 C.R1001-F, v1.0, section 4.5 for layout details) + * + * @param smsData byte array of raw encoded SMS bearer data. + * @param serviceCategory the envelope service category (for CMAS alert handling) + * @return an instance of BearerData. + */ + public static BearerData decode(byte[] smsData, int serviceCategory) { + try { + BitwiseInputStream inStream = new BitwiseInputStream(smsData); + BearerData bData = new BearerData(); + int foundSubparamMask = 0; + while (inStream.available() > 0) { + int subparamId = inStream.read(8); + int subparamIdBit = 1 << subparamId; + // int is 4 bytes. This duplicate check has a limit to Id number up to 32 (4*8) + // as 32th bit is the max bit in int. + // Per 3GPP2 C.S0015-B Table 4.5-1 Bearer Data Subparameter Identifiers: + // last defined subparam ID is 23 (00010111 = 0x17 = 23). + // Only do duplicate subparam ID check if subparam is within defined value as + // reserved subparams are just skipped. + if ((foundSubparamMask & subparamIdBit) != 0 && + (subparamId >= SUBPARAM_MESSAGE_IDENTIFIER && + subparamId <= SUBPARAM_ID_LAST_DEFINED)) { + throw new CodingException("illegal duplicate subparameter (" + + subparamId + ")"); + } + boolean decodeSuccess; + switch (subparamId) { + case SUBPARAM_MESSAGE_IDENTIFIER: + decodeSuccess = decodeMessageId(bData, inStream); + break; + case SUBPARAM_USER_DATA: + decodeSuccess = decodeUserData(bData, inStream); + break; + case SUBPARAM_USER_RESPONSE_CODE: + decodeSuccess = decodeUserResponseCode(bData, inStream); + break; + case SUBPARAM_REPLY_OPTION: + decodeSuccess = decodeReplyOption(bData, inStream); + break; + case SUBPARAM_NUMBER_OF_MESSAGES: + decodeSuccess = decodeMsgCount(bData, inStream); + break; + case SUBPARAM_CALLBACK_NUMBER: + decodeSuccess = decodeCallbackNumber(bData, inStream); + break; + case SUBPARAM_MESSAGE_STATUS: + decodeSuccess = decodeMsgStatus(bData, inStream); + break; + case SUBPARAM_MESSAGE_CENTER_TIME_STAMP: + decodeSuccess = decodeMsgCenterTimeStamp(bData, inStream); + break; + case SUBPARAM_VALIDITY_PERIOD_ABSOLUTE: + decodeSuccess = decodeValidityAbs(bData, inStream); + break; + case SUBPARAM_VALIDITY_PERIOD_RELATIVE: + decodeSuccess = decodeValidityRel(bData, inStream); + break; + case SUBPARAM_DEFERRED_DELIVERY_TIME_ABSOLUTE: + decodeSuccess = decodeDeferredDeliveryAbs(bData, inStream); + break; + case SUBPARAM_DEFERRED_DELIVERY_TIME_RELATIVE: + decodeSuccess = decodeDeferredDeliveryRel(bData, inStream); + break; + case SUBPARAM_PRIVACY_INDICATOR: + decodeSuccess = decodePrivacyIndicator(bData, inStream); + break; + case SUBPARAM_LANGUAGE_INDICATOR: + decodeSuccess = decodeLanguageIndicator(bData, inStream); + break; + case SUBPARAM_MESSAGE_DISPLAY_MODE: + decodeSuccess = decodeDisplayMode(bData, inStream); + break; + case SUBPARAM_PRIORITY_INDICATOR: + decodeSuccess = decodePriorityIndicator(bData, inStream); + break; + case SUBPARAM_ALERT_ON_MESSAGE_DELIVERY: + decodeSuccess = decodeMsgDeliveryAlert(bData, inStream); + break; + case SUBPARAM_MESSAGE_DEPOSIT_INDEX: + decodeSuccess = decodeDepositIndex(bData, inStream); + break; + case SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA: + decodeSuccess = decodeServiceCategoryProgramData(bData, inStream); + break; + default: + decodeSuccess = decodeReserved(bData, inStream, subparamId); + } + if (decodeSuccess && + (subparamId >= SUBPARAM_MESSAGE_IDENTIFIER && + subparamId <= SUBPARAM_ID_LAST_DEFINED)) { + foundSubparamMask |= subparamIdBit; + } + } + if ((foundSubparamMask & (1 << SUBPARAM_MESSAGE_IDENTIFIER)) == 0) { + throw new CodingException("missing MESSAGE_IDENTIFIER subparam"); + } + if (bData.userData != null) { + if (isCmasAlertCategory(serviceCategory)) { + decodeCmasUserData(bData, serviceCategory); + } else if (bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) { + if ((foundSubparamMask ^ + (1 << SUBPARAM_MESSAGE_IDENTIFIER) ^ + (1 << SUBPARAM_USER_DATA)) + != 0) { + Rlog.e(LOG_TAG, "IS-91 must occur without extra subparams (" + + foundSubparamMask + ")"); + } + decodeIs91(bData); + } else { + decodeUserDataPayload(bData.userData, bData.hasUserDataHeader); + } + } + return bData; + } catch (BitwiseInputStream.AccessException ex) { + Rlog.e(LOG_TAG, "BearerData decode failed: " + ex); + } catch (CodingException ex) { + Rlog.e(LOG_TAG, "BearerData decode failed: " + ex); + } + return null; + } +} diff --git a/telephony/java/com/android/internal/telephony/cdma/CdmaSmsAddress.java b/telephony/java/com/android/internal/telephony/cdma/CdmaSmsAddress.java new file mode 100644 index 000000000000..5f2e561b687f --- /dev/null +++ b/telephony/java/com/android/internal/telephony/cdma/CdmaSmsAddress.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma.sms; + +import android.util.SparseBooleanArray; + +import com.android.internal.telephony.SmsAddress; +import com.android.internal.telephony.cdma.sms.UserData; +import com.android.internal.util.HexDump; + +public class CdmaSmsAddress extends SmsAddress { + + /** + * Digit Mode Indicator is a 1-bit value that indicates whether + * the address digits are 4-bit DTMF codes or 8-bit codes. (See + * 3GPP2 C.S0015-B, v2, 3.4.3.3) + */ + static public final int DIGIT_MODE_4BIT_DTMF = 0x00; + static public final int DIGIT_MODE_8BIT_CHAR = 0x01; + + public int digitMode; + + /** + * Number Mode Indicator is 1-bit value that indicates whether the + * address type is a data network address or not. (See 3GPP2 + * C.S0015-B, v2, 3.4.3.3) + */ + static public final int NUMBER_MODE_NOT_DATA_NETWORK = 0x00; + static public final int NUMBER_MODE_DATA_NETWORK = 0x01; + + public int numberMode; + + /** + * Number Types for data networks. + * (See 3GPP2 C.S005-D, table2.7.1.3.2.4-2 for complete table) + * (See 3GPP2 C.S0015-B, v2, 3.4.3.3 for data network subset) + * NOTE: value is stored in the parent class ton field. + */ + static public final int TON_UNKNOWN = 0x00; + static public final int TON_INTERNATIONAL_OR_IP = 0x01; + static public final int TON_NATIONAL_OR_EMAIL = 0x02; + static public final int TON_NETWORK = 0x03; + static public final int TON_SUBSCRIBER = 0x04; + static public final int TON_ALPHANUMERIC = 0x05; + static public final int TON_ABBREVIATED = 0x06; + static public final int TON_RESERVED = 0x07; + + /** + * Maximum lengths for fields as defined in ril_cdma_sms.h. + */ + static public final int SMS_ADDRESS_MAX = 36; + static public final int SMS_SUBADDRESS_MAX = 36; + + /** + * This field shall be set to the number of address digits + * (See 3GPP2 C.S0015-B, v2, 3.4.3.3) + */ + public int numberOfDigits; + + /** + * Numbering Plan identification is a 0 or 4-bit value that + * indicates which numbering plan identification is set. (See + * 3GPP2, C.S0015-B, v2, 3.4.3.3 and C.S005-D, table2.7.1.3.2.4-3) + */ + static public final int NUMBERING_PLAN_UNKNOWN = 0x0; + static public final int NUMBERING_PLAN_ISDN_TELEPHONY = 0x1; + //static protected final int NUMBERING_PLAN_DATA = 0x3; + //static protected final int NUMBERING_PLAN_TELEX = 0x4; + //static protected final int NUMBERING_PLAN_PRIVATE = 0x9; + + public int numberPlan; + + /** + * NOTE: the parsed string address and the raw byte array values + * are stored in the parent class address and origBytes fields, + * respectively. + */ + + public CdmaSmsAddress(){ + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("CdmaSmsAddress "); + builder.append("{ digitMode=" + digitMode); + builder.append(", numberMode=" + numberMode); + builder.append(", numberPlan=" + numberPlan); + builder.append(", numberOfDigits=" + numberOfDigits); + builder.append(", ton=" + ton); + builder.append(", address=\"" + address + "\""); + builder.append(", origBytes=" + HexDump.toHexString(origBytes)); + builder.append(" }"); + return builder.toString(); + } + + /* + * TODO(cleanup): Refactor the parsing for addresses to better + * share code and logic with GSM. Also, gather all DTMF/BCD + * processing code in one place. + */ + + private static byte[] parseToDtmf(String address) { + int digits = address.length(); + byte[] result = new byte[digits]; + for (int i = 0; i < digits; i++) { + char c = address.charAt(i); + int val = 0; + if ((c >= '1') && (c <= '9')) val = c - '0'; + else if (c == '0') val = 10; + else if (c == '*') val = 11; + else if (c == '#') val = 12; + else return null; + result[i] = (byte)val; + } + return result; + } + + private static final char[] numericCharsDialable = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#' + }; + + private static final char[] numericCharsSugar = { + '(', ')', ' ', '-', '+', '.', '/', '\\' + }; + + private static final SparseBooleanArray numericCharDialableMap = new SparseBooleanArray ( + numericCharsDialable.length + numericCharsSugar.length); + static { + for (int i = 0; i < numericCharsDialable.length; i++) { + numericCharDialableMap.put(numericCharsDialable[i], true); + } + for (int i = 0; i < numericCharsSugar.length; i++) { + numericCharDialableMap.put(numericCharsSugar[i], false); + } + } + + /** + * Given a numeric address string, return the string without + * syntactic sugar, meaning parens, spaces, hyphens/minuses, or + * plus signs. If the input string contains non-numeric + * non-punctuation characters, return null. + */ + private static String filterNumericSugar(String address) { + StringBuilder builder = new StringBuilder(); + int len = address.length(); + for (int i = 0; i < len; i++) { + char c = address.charAt(i); + int mapIndex = numericCharDialableMap.indexOfKey(c); + if (mapIndex < 0) return null; + if (! numericCharDialableMap.valueAt(mapIndex)) continue; + builder.append(c); + } + return builder.toString(); + } + + /** + * Given a string, return the string without whitespace, + * including CR/LF. + */ + private static String filterWhitespace(String address) { + StringBuilder builder = new StringBuilder(); + int len = address.length(); + for (int i = 0; i < len; i++) { + char c = address.charAt(i); + if ((c == ' ') || (c == '\r') || (c == '\n') || (c == '\t')) continue; + builder.append(c); + } + return builder.toString(); + } + + /** + * Given a string, create a corresponding CdmaSmsAddress object. + * + * The result will be null if the input string is not + * representable using printable ASCII. + * + * For numeric addresses, the string is cleaned up by removing + * common punctuation. For alpha addresses, the string is cleaned + * up by removing whitespace. + */ + public static CdmaSmsAddress parse(String address) { + CdmaSmsAddress addr = new CdmaSmsAddress(); + addr.address = address; + addr.ton = CdmaSmsAddress.TON_UNKNOWN; + byte[] origBytes = null; + String filteredAddr = filterNumericSugar(address); + if (filteredAddr != null) { + origBytes = parseToDtmf(filteredAddr); + } + if (origBytes != null) { + addr.digitMode = DIGIT_MODE_4BIT_DTMF; + addr.numberMode = NUMBER_MODE_NOT_DATA_NETWORK; + if (address.indexOf('+') != -1) { + addr.ton = TON_INTERNATIONAL_OR_IP; + } + } else { + filteredAddr = filterWhitespace(address); + origBytes = UserData.stringToAscii(filteredAddr); + if (origBytes == null) { + return null; + } + addr.digitMode = DIGIT_MODE_8BIT_CHAR; + addr.numberMode = NUMBER_MODE_DATA_NETWORK; + if (address.indexOf('@') != -1) { + addr.ton = TON_NATIONAL_OR_EMAIL; + } + } + addr.origBytes = origBytes; + addr.numberOfDigits = origBytes.length; + return addr; + } + +} diff --git a/telephony/java/com/android/internal/telephony/cdma/CdmaSmsSubaddress.java b/telephony/java/com/android/internal/telephony/cdma/CdmaSmsSubaddress.java new file mode 100644 index 000000000000..0d5b502b9102 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/cdma/CdmaSmsSubaddress.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2010 The Android Open Source Project. All rights reserved. + * + * 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 com.android.internal.telephony.cdma.sms; + +public class CdmaSmsSubaddress { + public int type; + + public byte odd; + + public byte[] origBytes; +} + diff --git a/telephony/java/com/android/internal/telephony/cdma/SmsEnvelope.java b/telephony/java/com/android/internal/telephony/cdma/SmsEnvelope.java new file mode 100644 index 000000000000..f73df56a9ba6 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/cdma/SmsEnvelope.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma.sms; + + +import com.android.internal.telephony.cdma.sms.CdmaSmsSubaddress; + +public final class SmsEnvelope { + /** + * Message Types + * (See 3GPP2 C.S0015-B 3.4.1) + */ + static public final int MESSAGE_TYPE_POINT_TO_POINT = 0x00; + static public final int MESSAGE_TYPE_BROADCAST = 0x01; + static public final int MESSAGE_TYPE_ACKNOWLEDGE = 0x02; + + /** + * Supported Teleservices + * (See 3GPP2 N.S0005 and TIA-41) + */ + static public final int TELESERVICE_NOT_SET = 0x0000; + static public final int TELESERVICE_WMT = 0x1002; + static public final int TELESERVICE_VMN = 0x1003; + static public final int TELESERVICE_WAP = 0x1004; + static public final int TELESERVICE_WEMT = 0x1005; + static public final int TELESERVICE_SCPT = 0x1006; + + /** + * The following are defined as extensions to the standard teleservices + */ + // Voice mail notification through Message Waiting Indication in CDMA mode or Analog mode. + // Defined in 3GPP2 C.S-0005, 3.7.5.6, an Info Record containing an 8-bit number with the + // number of messages waiting, it's used by some CDMA carriers for a voice mail count. + static public final int TELESERVICE_MWI = 0x40000; + + // Service Categories for Cell Broadcast, see 3GPP2 C.R1001 table 9.3.1-1 + // static final int SERVICE_CATEGORY_EMERGENCY = 0x0001; + //... + + // CMAS alert service category assignments, see 3GPP2 C.R1001 table 9.3.3-1 + public static final int SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT = 0x1000; + public static final int SERVICE_CATEGORY_CMAS_EXTREME_THREAT = 0x1001; + public static final int SERVICE_CATEGORY_CMAS_SEVERE_THREAT = 0x1002; + public static final int SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY = 0x1003; + public static final int SERVICE_CATEGORY_CMAS_TEST_MESSAGE = 0x1004; + public static final int SERVICE_CATEGORY_CMAS_LAST_RESERVED_VALUE = 0x10ff; + + /** + * Provides the type of a SMS message like point to point, broadcast or acknowledge + */ + public int messageType; + + /** + * The 16-bit Teleservice parameter identifies which upper layer service access point is sending + * or receiving the message. + * (See 3GPP2 C.S0015-B, v2, 3.4.3.1) + */ + public int teleService = TELESERVICE_NOT_SET; + + /** + * The 16-bit service category parameter identifies the type of service provided + * by the SMS message. + * (See 3GPP2 C.S0015-B, v2, 3.4.3.2) + */ + public int serviceCategory; + + /** + * The origination address identifies the originator of the SMS message. + * (See 3GPP2 C.S0015-B, v2, 3.4.3.3) + */ + public CdmaSmsAddress origAddress; + + /** + * The destination address identifies the target of the SMS message. + * (See 3GPP2 C.S0015-B, v2, 3.4.3.3) + */ + public CdmaSmsAddress destAddress; + + /** + * The origination subaddress identifies the originator of the SMS message. + * (See 3GPP2 C.S0015-B, v2, 3.4.3.4) + */ + public CdmaSmsSubaddress origSubaddress; + + /** + * The 6-bit bearer reply parameter is used to request the return of a + * SMS Acknowledge Message. + * (See 3GPP2 C.S0015-B, v2, 3.4.3.5) + */ + public int bearerReply; + + /** + * Cause Code values: + * The cause code parameters are an indication whether an SMS error has occurred and if so, + * whether the condition is considered temporary or permanent. + * ReplySeqNo 6-bit value, + * ErrorClass 2-bit value, + * CauseCode 0-bit or 8-bit value + * (See 3GPP2 C.S0015-B, v2, 3.4.3.6) + */ + public byte replySeqNo; + public byte errorClass; + public byte causeCode; + + /** + * encoded bearer data + * (See 3GPP2 C.S0015-B, v2, 3.4.3.7) + */ + public byte[] bearerData; + + public SmsEnvelope() { + // nothing to see here + } + +} + diff --git a/telephony/java/com/android/internal/telephony/cdma/SmsMessage.java b/telephony/java/com/android/internal/telephony/cdma/SmsMessage.java new file mode 100644 index 000000000000..0b75fa5caab2 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/cdma/SmsMessage.java @@ -0,0 +1,1058 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma; + +import android.os.Parcel; +import android.os.SystemProperties; +import android.telephony.PhoneNumberUtils; +import android.telephony.SmsCbLocation; +import android.telephony.SmsCbMessage; +import android.telephony.TelephonyManager; +import android.telephony.cdma.CdmaSmsCbProgramData; +import android.telephony.Rlog; +import android.util.Log; +import android.text.TextUtils; +import android.content.res.Resources; + +import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails; +import com.android.internal.telephony.SmsConstants; +import com.android.internal.telephony.SmsHeader; +import com.android.internal.telephony.SmsMessageBase; +import com.android.internal.telephony.TelephonyProperties; +import com.android.internal.telephony.cdma.sms.BearerData; +import com.android.internal.telephony.cdma.sms.CdmaSmsAddress; +import com.android.internal.telephony.cdma.sms.CdmaSmsSubaddress; +import com.android.internal.telephony.cdma.sms.SmsEnvelope; +import com.android.internal.telephony.cdma.sms.UserData; +import com.android.internal.telephony.uicc.IccUtils; +import com.android.internal.util.BitwiseInputStream; +import com.android.internal.util.HexDump; +import com.android.internal.telephony.Sms7BitEncodingTranslator; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; + +/** + * TODO(cleanup): these constants are disturbing... are they not just + * different interpretations on one number? And if we did not have + * terrible class name overlap, they would not need to be directly + * imported like this. The class in this file could just as well be + * named CdmaSmsMessage, could it not? + */ + +/** + * TODO(cleanup): internally returning null in many places makes + * debugging very hard (among many other reasons) and should be made + * more meaningful (replaced with exceptions for example). Null + * returns should only occur at the very outside of the module/class + * scope. + */ + +/** + * A Short Message Service message. + * + */ +public class SmsMessage extends SmsMessageBase { + static final String LOG_TAG = "SmsMessage"; + static private final String LOGGABLE_TAG = "CDMA:SMS"; + private static final boolean VDBG = false; + + private final static byte TELESERVICE_IDENTIFIER = 0x00; + private final static byte SERVICE_CATEGORY = 0x01; + private final static byte ORIGINATING_ADDRESS = 0x02; + private final static byte ORIGINATING_SUB_ADDRESS = 0x03; + private final static byte DESTINATION_ADDRESS = 0x04; + private final static byte DESTINATION_SUB_ADDRESS = 0x05; + private final static byte BEARER_REPLY_OPTION = 0x06; + private final static byte CAUSE_CODES = 0x07; + private final static byte BEARER_DATA = 0x08; + + /** + * Status of a previously submitted SMS. + * This field applies to SMS Delivery Acknowledge messages. 0 indicates success; + * Here, the error class is defined by the bits from 9-8, the status code by the bits from 7-0. + * See C.S0015-B, v2.0, 4.5.21 for a detailed description of possible values. + */ + private int status; + + /** Specifies if a return of an acknowledgment is requested for send SMS */ + private static final int RETURN_NO_ACK = 0; + private static final int RETURN_ACK = 1; + + private SmsEnvelope mEnvelope; + private BearerData mBearerData; + + public static class SubmitPdu extends SubmitPduBase { + } + + /** + * Create an SmsMessage from a raw PDU. + * Note: In CDMA the PDU is just a byte representation of the received Sms. + */ + public static SmsMessage createFromPdu(byte[] pdu) { + SmsMessage msg = new SmsMessage(); + + try { + msg.parsePdu(pdu); + return msg; + } catch (RuntimeException ex) { + Rlog.e(LOG_TAG, "SMS PDU parsing failed: ", ex); + return null; + } catch (OutOfMemoryError e) { + Log.e(LOG_TAG, "SMS PDU parsing failed with out of memory: ", e); + return null; + } + } + + /** + * Create a "raw" CDMA SmsMessage from a Parcel that was forged in ril.cpp. + * Note: Only primitive fields are set. + */ + public static SmsMessage newFromParcel(Parcel p) { + // Note: Parcel.readByte actually reads one Int and masks to byte + SmsMessage msg = new SmsMessage(); + SmsEnvelope env = new SmsEnvelope(); + CdmaSmsAddress addr = new CdmaSmsAddress(); + CdmaSmsSubaddress subaddr = new CdmaSmsSubaddress(); + byte[] data; + byte count; + int countInt; + int addressDigitMode; + + //currently not supported by the modem-lib: env.mMessageType + env.teleService = p.readInt(); //p_cur->uTeleserviceID + + if (0 != p.readByte()) { //p_cur->bIsServicePresent + env.messageType = SmsEnvelope.MESSAGE_TYPE_BROADCAST; + } + else { + if (SmsEnvelope.TELESERVICE_NOT_SET == env.teleService) { + // assume type ACK + env.messageType = SmsEnvelope.MESSAGE_TYPE_ACKNOWLEDGE; + } else { + env.messageType = SmsEnvelope.MESSAGE_TYPE_POINT_TO_POINT; + } + } + env.serviceCategory = p.readInt(); //p_cur->uServicecategory + + // address + addressDigitMode = p.readInt(); + addr.digitMode = (byte) (0xFF & addressDigitMode); //p_cur->sAddress.digit_mode + addr.numberMode = (byte) (0xFF & p.readInt()); //p_cur->sAddress.number_mode + addr.ton = p.readInt(); //p_cur->sAddress.number_type + addr.numberPlan = (byte) (0xFF & p.readInt()); //p_cur->sAddress.number_plan + count = p.readByte(); //p_cur->sAddress.number_of_digits + addr.numberOfDigits = count; + data = new byte[count]; + //p_cur->sAddress.digits[digitCount] + for (int index=0; index < count; index++) { + data[index] = p.readByte(); + + // convert the value if it is 4-bit DTMF to 8 bit + if (addressDigitMode == CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF) { + data[index] = msg.convertDtmfToAscii(data[index]); + } + } + + addr.origBytes = data; + + subaddr.type = p.readInt(); // p_cur->sSubAddress.subaddressType + subaddr.odd = p.readByte(); // p_cur->sSubAddress.odd + count = p.readByte(); // p_cur->sSubAddress.number_of_digits + + if (count < 0) { + count = 0; + } + + // p_cur->sSubAddress.digits[digitCount] : + + data = new byte[count]; + + for (int index = 0; index < count; ++index) { + data[index] = p.readByte(); + } + + subaddr.origBytes = data; + + /* currently not supported by the modem-lib: + env.bearerReply + env.replySeqNo + env.errorClass + env.causeCode + */ + + // bearer data + countInt = p.readInt(); //p_cur->uBearerDataLen + if (countInt < 0) { + countInt = 0; + } + + data = new byte[countInt]; + for (int index=0; index < countInt; index++) { + data[index] = p.readByte(); + } + // BD gets further decoded when accessed in SMSDispatcher + env.bearerData = data; + + // link the the filled objects to the SMS + env.origAddress = addr; + env.origSubaddress = subaddr; + msg.mOriginatingAddress = addr; + msg.mEnvelope = env; + + // create byte stream representation for transportation through the layers. + msg.createPdu(); + + return msg; + } + + /** + * Create an SmsMessage from an SMS EF record. + * + * @param index Index of SMS record. This should be index in ArrayList + * returned by RuimSmsInterfaceManager.getAllMessagesFromIcc + 1. + * @param data Record data. + * @return An SmsMessage representing the record. + * + * @hide + */ + public static SmsMessage createFromEfRecord(int index, byte[] data) { + try { + SmsMessage msg = new SmsMessage(); + + msg.mIndexOnIcc = index; + + // First byte is status: RECEIVED_READ, RECEIVED_UNREAD, STORED_SENT, + // or STORED_UNSENT + // See 3GPP2 C.S0023 3.4.27 + if ((data[0] & 1) == 0) { + Rlog.w(LOG_TAG, "SMS parsing failed: Trying to parse a free record"); + return null; + } else { + msg.mStatusOnIcc = data[0] & 0x07; + } + + // Second byte is the MSG_LEN, length of the message + // See 3GPP2 C.S0023 3.4.27 + int size = data[1]; + + // Note: Data may include trailing FF's. That's OK; message + // should still parse correctly. + byte[] pdu = new byte[size]; + System.arraycopy(data, 2, pdu, 0, size); + // the message has to be parsed before it can be displayed + // see gsm.SmsMessage + msg.parsePduFromEfRecord(pdu); + return msg; + } catch (RuntimeException ex) { + Rlog.e(LOG_TAG, "SMS PDU parsing failed: ", ex); + return null; + } + + } + + /** + * Note: This function is a GSM specific functionality which is not supported in CDMA mode. + */ + public static int getTPLayerLengthForPDU(String pdu) { + Rlog.w(LOG_TAG, "getTPLayerLengthForPDU: is not supported in CDMA mode."); + return 0; + } + + /** + * TODO(cleanup): why do getSubmitPdu methods take an scAddr input + * and do nothing with it? GSM allows us to specify a SC (eg, + * when responding to an SMS that explicitly requests the response + * is sent to a specific SC), or pass null to use the default + * value. Is there no similar notion in CDMA? Or do we just not + * have it hooked up? + */ + + /** + * Get an SMS-SUBMIT PDU for a destination address and a message + * + * @param scAddr Service Centre address. Null means use default. + * @param destAddr Address of the recipient. + * @param message String representation of the message payload. + * @param statusReportRequested Indicates whether a report is requested for this message. + * @param smsHeader Array containing the data for the User Data Header, preceded + * by the Element Identifiers. + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + * @hide + */ + public static SubmitPdu getSubmitPdu(String scAddr, String destAddr, String message, + boolean statusReportRequested, SmsHeader smsHeader) { + + /** + * TODO(cleanup): Do we really want silent failure like this? + * Would it not be much more reasonable to make sure we don't + * call this function if we really want nothing done? + */ + if (message == null || destAddr == null) { + return null; + } + + UserData uData = new UserData(); + uData.payloadStr = message; + uData.userDataHeader = smsHeader; + return privateGetSubmitPdu(destAddr, statusReportRequested, uData); + } + + /** + * Get an SMS-SUBMIT PDU for a data message to a destination address and port. + * + * @param scAddr Service Centre address. null == use default + * @param destAddr the address of the destination for the message + * @param destPort the port to deliver the message to at the + * destination + * @param data the data for the message + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + */ + public static SubmitPdu getSubmitPdu(String scAddr, String destAddr, int destPort, + byte[] data, boolean statusReportRequested) { + + /** + * TODO(cleanup): this is not a general-purpose SMS creation + * method, but rather something specialized to messages + * containing OCTET encoded (meaning non-human-readable) user + * data. The name should reflect that, and not just overload. + */ + + SmsHeader.PortAddrs portAddrs = new SmsHeader.PortAddrs(); + portAddrs.destPort = destPort; + portAddrs.origPort = 0; + portAddrs.areEightBits = false; + + SmsHeader smsHeader = new SmsHeader(); + smsHeader.portAddrs = portAddrs; + + UserData uData = new UserData(); + uData.userDataHeader = smsHeader; + uData.msgEncoding = UserData.ENCODING_OCTET; + uData.msgEncodingSet = true; + uData.payload = data; + + return privateGetSubmitPdu(destAddr, statusReportRequested, uData); + } + + /** + * Get an SMS-SUBMIT PDU for a data message to a destination address & port + * + * @param destAddr the address of the destination for the message + * @param userData the data for the message + * @param statusReportRequested Indicates whether a report is requested for this message. + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + */ + public static SubmitPdu getSubmitPdu(String destAddr, UserData userData, + boolean statusReportRequested) { + return privateGetSubmitPdu(destAddr, statusReportRequested, userData); + } + + /** + * Note: This function is a GSM specific functionality which is not supported in CDMA mode. + */ + @Override + public int getProtocolIdentifier() { + Rlog.w(LOG_TAG, "getProtocolIdentifier: is not supported in CDMA mode."); + // (3GPP TS 23.040): "no interworking, but SME to SME protocol": + return 0; + } + + /** + * Note: This function is a GSM specific functionality which is not supported in CDMA mode. + */ + @Override + public boolean isReplace() { + Rlog.w(LOG_TAG, "isReplace: is not supported in CDMA mode."); + return false; + } + + /** + * {@inheritDoc} + * Note: This function is a GSM specific functionality which is not supported in CDMA mode. + */ + @Override + public boolean isCphsMwiMessage() { + Rlog.w(LOG_TAG, "isCphsMwiMessage: is not supported in CDMA mode."); + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isMWIClearMessage() { + return ((mBearerData != null) && (mBearerData.numberOfMessages == 0)); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isMWISetMessage() { + return ((mBearerData != null) && (mBearerData.numberOfMessages > 0)); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isMwiDontStore() { + return ((mBearerData != null) && + (mBearerData.numberOfMessages > 0) && + (mBearerData.userData == null)); + } + + /** + * Returns the status for a previously submitted message. + * For not interfering with status codes from GSM, this status code is + * shifted to the bits 31-16. + */ + @Override + public int getStatus() { + return (status << 16); + } + + /** Return true iff the bearer data message type is DELIVERY_ACK. */ + @Override + public boolean isStatusReportMessage() { + return (mBearerData.messageType == BearerData.MESSAGE_TYPE_DELIVERY_ACK); + } + + /** + * Note: This function is a GSM specific functionality which is not supported in CDMA mode. + */ + @Override + public boolean isReplyPathPresent() { + Rlog.w(LOG_TAG, "isReplyPathPresent: is not supported in CDMA mode."); + return false; + } + + /** + * Calculate the number of septets needed to encode the message. + * + * @param messageBody the message to encode + * @param use7bitOnly ignore (but still count) illegal characters if true + * @param isEntireMsg indicates if this is entire msg or a segment in multipart msg + * @return TextEncodingDetails + */ + public static TextEncodingDetails calculateLength(CharSequence messageBody, + boolean use7bitOnly, boolean isEntireMsg) { + CharSequence newMsgBody = null; + Resources r = Resources.getSystem(); + if (r.getBoolean(com.android.internal.R.bool.config_sms_force_7bit_encoding)) { + newMsgBody = Sms7BitEncodingTranslator.translate(messageBody); + } + if (TextUtils.isEmpty(newMsgBody)) { + newMsgBody = messageBody; + } + return BearerData.calcTextEncodingDetails(newMsgBody, use7bitOnly, isEntireMsg); + } + + /** + * Returns the teleservice type of the message. + * @return the teleservice: + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_NOT_SET}, + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_WMT}, + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_WEMT}, + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_VMN}, + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_WAP} + */ + public int getTeleService() { + return mEnvelope.teleService; + } + + /** + * Returns the message type of the message. + * @return the message type: + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#MESSAGE_TYPE_POINT_TO_POINT}, + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#MESSAGE_TYPE_BROADCAST}, + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#MESSAGE_TYPE_ACKNOWLEDGE}, + */ + public int getMessageType() { + // NOTE: mEnvelope.messageType is not set correctly for cell broadcasts with some RILs. + // Use the service category parameter to detect CMAS and other cell broadcast messages. + if (mEnvelope.serviceCategory != 0) { + return SmsEnvelope.MESSAGE_TYPE_BROADCAST; + } else { + return SmsEnvelope.MESSAGE_TYPE_POINT_TO_POINT; + } + } + + /** + * Decodes pdu to an empty SMS object. + * In the CDMA case the pdu is just an internal byte stream representation + * of the SMS Java-object. + * @see #createPdu() + */ + private void parsePdu(byte[] pdu) { + ByteArrayInputStream bais = new ByteArrayInputStream(pdu); + DataInputStream dis = new DataInputStream(bais); + int length; + int bearerDataLength; + SmsEnvelope env = new SmsEnvelope(); + CdmaSmsAddress addr = new CdmaSmsAddress(); + + try { + env.messageType = dis.readInt(); + env.teleService = dis.readInt(); + env.serviceCategory = dis.readInt(); + + addr.digitMode = dis.readByte(); + addr.numberMode = dis.readByte(); + addr.ton = dis.readByte(); + addr.numberPlan = dis.readByte(); + + length = dis.readUnsignedByte(); + addr.numberOfDigits = length; + + // sanity check on the length + if (length > pdu.length) { + throw new RuntimeException( + "createFromPdu: Invalid pdu, addr.numberOfDigits " + length + + " > pdu len " + pdu.length); + } + addr.origBytes = new byte[length]; + dis.read(addr.origBytes, 0, length); // digits + + env.bearerReply = dis.readInt(); + // CauseCode values: + env.replySeqNo = dis.readByte(); + env.errorClass = dis.readByte(); + env.causeCode = dis.readByte(); + + //encoded BearerData: + bearerDataLength = dis.readInt(); + // sanity check on the length + if (bearerDataLength > pdu.length) { + throw new RuntimeException( + "createFromPdu: Invalid pdu, bearerDataLength " + bearerDataLength + + " > pdu len " + pdu.length); + } + env.bearerData = new byte[bearerDataLength]; + dis.read(env.bearerData, 0, bearerDataLength); + dis.close(); + } catch (IOException ex) { + throw new RuntimeException( + "createFromPdu: conversion from byte array to object failed: " + ex, ex); + } catch (Exception ex) { + Rlog.e(LOG_TAG, "createFromPdu: conversion from byte array to object failed: " + ex); + } + + // link the filled objects to this SMS + mOriginatingAddress = addr; + env.origAddress = addr; + mEnvelope = env; + mPdu = pdu; + + parseSms(); + } + + /** + * Decodes 3GPP2 sms stored in CSIM/RUIM cards As per 3GPP2 C.S0015-0 + */ + private void parsePduFromEfRecord(byte[] pdu) { + ByteArrayInputStream bais = new ByteArrayInputStream(pdu); + DataInputStream dis = new DataInputStream(bais); + SmsEnvelope env = new SmsEnvelope(); + CdmaSmsAddress addr = new CdmaSmsAddress(); + CdmaSmsSubaddress subAddr = new CdmaSmsSubaddress(); + + try { + env.messageType = dis.readByte(); + + while (dis.available() > 0) { + int parameterId = dis.readByte(); + int parameterLen = dis.readUnsignedByte(); + byte[] parameterData = new byte[parameterLen]; + + switch (parameterId) { + case TELESERVICE_IDENTIFIER: + /* + * 16 bit parameter that identifies which upper layer + * service access point is sending or should receive + * this message + */ + env.teleService = dis.readUnsignedShort(); + Rlog.i(LOG_TAG, "teleservice = " + env.teleService); + break; + case SERVICE_CATEGORY: + /* + * 16 bit parameter that identifies type of service as + * in 3GPP2 C.S0015-0 Table 3.4.3.2-1 + */ + env.serviceCategory = dis.readUnsignedShort(); + break; + case ORIGINATING_ADDRESS: + case DESTINATION_ADDRESS: + dis.read(parameterData, 0, parameterLen); + BitwiseInputStream addrBis = new BitwiseInputStream(parameterData); + addr.digitMode = addrBis.read(1); + addr.numberMode = addrBis.read(1); + int numberType = 0; + if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) { + numberType = addrBis.read(3); + addr.ton = numberType; + + if (addr.numberMode == CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK) + addr.numberPlan = addrBis.read(4); + } + + addr.numberOfDigits = addrBis.read(8); + + byte[] data = new byte[addr.numberOfDigits]; + byte b = 0x00; + + if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF) { + /* As per 3GPP2 C.S0005-0 Table 2.7.1.3.2.4-4 */ + for (int index = 0; index < addr.numberOfDigits; index++) { + b = (byte) (0xF & addrBis.read(4)); + // convert the value if it is 4-bit DTMF to 8 + // bit + data[index] = convertDtmfToAscii(b); + } + } else if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) { + if (addr.numberMode == CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK) { + for (int index = 0; index < addr.numberOfDigits; index++) { + b = (byte) (0xFF & addrBis.read(8)); + data[index] = b; + } + + } else if (addr.numberMode == CdmaSmsAddress.NUMBER_MODE_DATA_NETWORK) { + if (numberType == 2) + Rlog.e(LOG_TAG, "TODO: Originating Addr is email id"); + else + Rlog.e(LOG_TAG, + "TODO: Originating Addr is data network address"); + } else { + Rlog.e(LOG_TAG, "Originating Addr is of incorrect type"); + } + } else { + Rlog.e(LOG_TAG, "Incorrect Digit mode"); + } + addr.origBytes = data; + Rlog.i(LOG_TAG, "Originating Addr=" + addr.toString()); + break; + case ORIGINATING_SUB_ADDRESS: + case DESTINATION_SUB_ADDRESS: + dis.read(parameterData, 0, parameterLen); + BitwiseInputStream subAddrBis = new BitwiseInputStream(parameterData); + subAddr.type = subAddrBis.read(3); + subAddr.odd = subAddrBis.readByteArray(1)[0]; + int subAddrLen = subAddrBis.read(8); + byte[] subdata = new byte[subAddrLen]; + for (int index = 0; index < subAddrLen; index++) { + b = (byte) (0xFF & subAddrBis.read(4)); + // convert the value if it is 4-bit DTMF to 8 bit + subdata[index] = convertDtmfToAscii(b); + } + subAddr.origBytes = subdata; + break; + case BEARER_REPLY_OPTION: + dis.read(parameterData, 0, parameterLen); + BitwiseInputStream replyOptBis = new BitwiseInputStream(parameterData); + env.bearerReply = replyOptBis.read(6); + break; + case CAUSE_CODES: + dis.read(parameterData, 0, parameterLen); + BitwiseInputStream ccBis = new BitwiseInputStream(parameterData); + env.replySeqNo = ccBis.readByteArray(6)[0]; + env.errorClass = ccBis.readByteArray(2)[0]; + if (env.errorClass != 0x00) + env.causeCode = ccBis.readByteArray(8)[0]; + break; + case BEARER_DATA: + dis.read(parameterData, 0, parameterLen); + env.bearerData = parameterData; + break; + default: + throw new Exception("unsupported parameterId (" + parameterId + ")"); + } + } + bais.close(); + dis.close(); + } catch (Exception ex) { + Rlog.e(LOG_TAG, "parsePduFromEfRecord: conversion from pdu to SmsMessage failed" + ex); + } + + // link the filled objects to this SMS + mOriginatingAddress = addr; + env.origAddress = addr; + env.origSubaddress = subAddr; + mEnvelope = env; + mPdu = pdu; + + parseSms(); + } + + /** + * Parses a SMS message from its BearerData stream. (mobile-terminated only) + */ + public void parseSms() { + // Message Waiting Info Record defined in 3GPP2 C.S-0005, 3.7.5.6 + // It contains only an 8-bit number with the number of messages waiting + if (mEnvelope.teleService == SmsEnvelope.TELESERVICE_MWI) { + mBearerData = new BearerData(); + if (mEnvelope.bearerData != null) { + mBearerData.numberOfMessages = 0x000000FF & mEnvelope.bearerData[0]; + } + if (VDBG) { + Rlog.d(LOG_TAG, "parseSms: get MWI " + + Integer.toString(mBearerData.numberOfMessages)); + } + return; + } + mBearerData = BearerData.decode(mEnvelope.bearerData); + if (Rlog.isLoggable(LOGGABLE_TAG, Log.VERBOSE)) { + Rlog.d(LOG_TAG, "MT raw BearerData = '" + + HexDump.toHexString(mEnvelope.bearerData) + "'"); + Rlog.d(LOG_TAG, "MT (decoded) BearerData = " + mBearerData); + } + mMessageRef = mBearerData.messageId; + if (mBearerData.userData != null) { + mUserData = mBearerData.userData.payload; + mUserDataHeader = mBearerData.userData.userDataHeader; + mMessageBody = mBearerData.userData.payloadStr; + } + + if (mOriginatingAddress != null) { + mOriginatingAddress.address = new String(mOriginatingAddress.origBytes); + if (mOriginatingAddress.ton == CdmaSmsAddress.TON_INTERNATIONAL_OR_IP) { + if (mOriginatingAddress.address.charAt(0) != '+') { + mOriginatingAddress.address = "+" + mOriginatingAddress.address; + } + } + if (VDBG) Rlog.v(LOG_TAG, "SMS originating address: " + + mOriginatingAddress.address); + } + + if (mBearerData.msgCenterTimeStamp != null) { + mScTimeMillis = mBearerData.msgCenterTimeStamp.toMillis(true); + } + + if (VDBG) Rlog.d(LOG_TAG, "SMS SC timestamp: " + mScTimeMillis); + + // Message Type (See 3GPP2 C.S0015-B, v2, 4.5.1) + if (mBearerData.messageType == BearerData.MESSAGE_TYPE_DELIVERY_ACK) { + // The BearerData MsgStatus subparameter should only be + // included for DELIVERY_ACK messages. If it occurred for + // other messages, it would be unclear what the status + // being reported refers to. The MsgStatus subparameter + // is primarily useful to indicate error conditions -- a + // message without this subparameter is assumed to + // indicate successful delivery (status == 0). + if (! mBearerData.messageStatusSet) { + Rlog.d(LOG_TAG, "DELIVERY_ACK message without msgStatus (" + + (mUserData == null ? "also missing" : "does have") + + " userData)."); + status = 0; + } else { + status = mBearerData.errorClass << 8; + status |= mBearerData.messageStatus; + } + } else if (mBearerData.messageType != BearerData.MESSAGE_TYPE_DELIVER) { + throw new RuntimeException("Unsupported message type: " + mBearerData.messageType); + } + + if (mMessageBody != null) { + if (VDBG) Rlog.v(LOG_TAG, "SMS message body: '" + mMessageBody + "'"); + parseMessageBody(); + } else if ((mUserData != null) && VDBG) { + Rlog.v(LOG_TAG, "SMS payload: '" + IccUtils.bytesToHexString(mUserData) + "'"); + } + } + + /** + * Parses a broadcast SMS, possibly containing a CMAS alert. + */ + public SmsCbMessage parseBroadcastSms() { + BearerData bData = BearerData.decode(mEnvelope.bearerData, mEnvelope.serviceCategory); + if (bData == null) { + Rlog.w(LOG_TAG, "BearerData.decode() returned null"); + return null; + } + + if (Rlog.isLoggable(LOGGABLE_TAG, Log.VERBOSE)) { + Rlog.d(LOG_TAG, "MT raw BearerData = " + HexDump.toHexString(mEnvelope.bearerData)); + } + + String plmn = TelephonyManager.getDefault().getNetworkOperator(); + SmsCbLocation location = new SmsCbLocation(plmn); + + return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP2, + SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE, bData.messageId, location, + mEnvelope.serviceCategory, bData.getLanguage(), bData.userData.payloadStr, + bData.priority, null, bData.cmasWarningInfo); + } + + /** + * {@inheritDoc} + */ + @Override + public SmsConstants.MessageClass getMessageClass() { + if (BearerData.DISPLAY_MODE_IMMEDIATE == mBearerData.displayMode ) { + return SmsConstants.MessageClass.CLASS_0; + } else { + return SmsConstants.MessageClass.UNKNOWN; + } + } + + /** + * Calculate the next message id, starting at 1 and iteratively + * incrementing within the range 1..65535 remembering the state + * via a persistent system property. (See C.S0015-B, v2.0, + * 4.3.1.5) Since this routine is expected to be accessed via via + * binder-call, and hence should be thread-safe, it has been + * synchronized. + */ + public synchronized static int getNextMessageId() { + // Testing and dialog with partners has indicated that + // msgId==0 is (sometimes?) treated specially by lower levels. + // Specifically, the ID is not preserved for delivery ACKs. + // Hence, avoid 0 -- constraining the range to 1..65535. + int msgId = SystemProperties.getInt(TelephonyProperties.PROPERTY_CDMA_MSG_ID, 1); + String nextMsgId = Integer.toString((msgId % 0xFFFF) + 1); + try{ + SystemProperties.set(TelephonyProperties.PROPERTY_CDMA_MSG_ID, nextMsgId); + if (Rlog.isLoggable(LOGGABLE_TAG, Log.VERBOSE)) { + Rlog.d(LOG_TAG, "next " + TelephonyProperties.PROPERTY_CDMA_MSG_ID + " = " + nextMsgId); + Rlog.d(LOG_TAG, "readback gets " + + SystemProperties.get(TelephonyProperties.PROPERTY_CDMA_MSG_ID)); + } + } catch(RuntimeException ex) { + Rlog.e(LOG_TAG, "set nextMessage ID failed: " + ex); + } + return msgId; + } + + /** + * Creates BearerData and Envelope from parameters for a Submit SMS. + * @return byte stream for SubmitPdu. + */ + private static SubmitPdu privateGetSubmitPdu(String destAddrStr, boolean statusReportRequested, + UserData userData) { + + /** + * TODO(cleanup): give this function a more meaningful name. + */ + + /** + * TODO(cleanup): Make returning null from the getSubmitPdu + * variations meaningful -- clean up the error feedback + * mechanism, and avoid null pointer exceptions. + */ + + /** + * North America Plus Code : + * Convert + code to 011 and dial out for international SMS + */ + CdmaSmsAddress destAddr = CdmaSmsAddress.parse( + PhoneNumberUtils.cdmaCheckAndProcessPlusCodeForSms(destAddrStr)); + if (destAddr == null) return null; + + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_SUBMIT; + + bearerData.messageId = getNextMessageId(); + + bearerData.deliveryAckReq = statusReportRequested; + bearerData.userAckReq = false; + bearerData.readAckReq = false; + bearerData.reportReq = false; + + bearerData.userData = userData; + + byte[] encodedBearerData = BearerData.encode(bearerData); + if (Rlog.isLoggable(LOGGABLE_TAG, Log.VERBOSE)) { + Rlog.d(LOG_TAG, "MO (encoded) BearerData = " + bearerData); + Rlog.d(LOG_TAG, "MO raw BearerData = '" + HexDump.toHexString(encodedBearerData) + "'"); + } + if (encodedBearerData == null) return null; + + int teleservice = bearerData.hasUserDataHeader ? + SmsEnvelope.TELESERVICE_WEMT : SmsEnvelope.TELESERVICE_WMT; + + SmsEnvelope envelope = new SmsEnvelope(); + envelope.messageType = SmsEnvelope.MESSAGE_TYPE_POINT_TO_POINT; + envelope.teleService = teleservice; + envelope.destAddress = destAddr; + envelope.bearerReply = RETURN_ACK; + envelope.bearerData = encodedBearerData; + + /** + * TODO(cleanup): envelope looks to be a pointless class, get + * rid of it. Also -- most of the envelope fields set here + * are ignored, why? + */ + + try { + /** + * TODO(cleanup): reference a spec and get rid of the ugly comments + */ + ByteArrayOutputStream baos = new ByteArrayOutputStream(100); + DataOutputStream dos = new DataOutputStream(baos); + dos.writeInt(envelope.teleService); + dos.writeInt(0); //servicePresent + dos.writeInt(0); //serviceCategory + dos.write(destAddr.digitMode); + dos.write(destAddr.numberMode); + dos.write(destAddr.ton); // number_type + dos.write(destAddr.numberPlan); + dos.write(destAddr.numberOfDigits); + dos.write(destAddr.origBytes, 0, destAddr.origBytes.length); // digits + // Subaddress is not supported. + dos.write(0); //subaddressType + dos.write(0); //subaddr_odd + dos.write(0); //subaddr_nbr_of_digits + dos.write(encodedBearerData.length); + dos.write(encodedBearerData, 0, encodedBearerData.length); + dos.close(); + + SubmitPdu pdu = new SubmitPdu(); + pdu.encodedMessage = baos.toByteArray(); + pdu.encodedScAddress = null; + return pdu; + } catch(IOException ex) { + Rlog.e(LOG_TAG, "creating SubmitPdu failed: " + ex); + } + return null; + } + + /** + * Creates byte array (pseudo pdu) from SMS object. + * Note: Do not call this method more than once per object! + */ + private void createPdu() { + SmsEnvelope env = mEnvelope; + CdmaSmsAddress addr = env.origAddress; + ByteArrayOutputStream baos = new ByteArrayOutputStream(100); + DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(baos)); + + try { + dos.writeInt(env.messageType); + dos.writeInt(env.teleService); + dos.writeInt(env.serviceCategory); + + dos.writeByte(addr.digitMode); + dos.writeByte(addr.numberMode); + dos.writeByte(addr.ton); + dos.writeByte(addr.numberPlan); + dos.writeByte(addr.numberOfDigits); + dos.write(addr.origBytes, 0, addr.origBytes.length); // digits + + dos.writeInt(env.bearerReply); + // CauseCode values: + dos.writeByte(env.replySeqNo); + dos.writeByte(env.errorClass); + dos.writeByte(env.causeCode); + //encoded BearerData: + dos.writeInt(env.bearerData.length); + dos.write(env.bearerData, 0, env.bearerData.length); + dos.close(); + + /** + * TODO(cleanup) -- The mPdu field is managed in + * a fragile manner, and it would be much nicer if + * accessing the serialized representation used a less + * fragile mechanism. Maybe the getPdu method could + * generate a representation if there was not yet one? + */ + + mPdu = baos.toByteArray(); + } catch (IOException ex) { + Rlog.e(LOG_TAG, "createPdu: conversion from object to byte array failed: " + ex); + } + } + + /** + * Converts a 4-Bit DTMF encoded symbol from the calling address number to ASCII character + */ + private byte convertDtmfToAscii(byte dtmfDigit) { + byte asciiDigit; + + switch (dtmfDigit) { + case 0: asciiDigit = 68; break; // 'D' + case 1: asciiDigit = 49; break; // '1' + case 2: asciiDigit = 50; break; // '2' + case 3: asciiDigit = 51; break; // '3' + case 4: asciiDigit = 52; break; // '4' + case 5: asciiDigit = 53; break; // '5' + case 6: asciiDigit = 54; break; // '6' + case 7: asciiDigit = 55; break; // '7' + case 8: asciiDigit = 56; break; // '8' + case 9: asciiDigit = 57; break; // '9' + case 10: asciiDigit = 48; break; // '0' + case 11: asciiDigit = 42; break; // '*' + case 12: asciiDigit = 35; break; // '#' + case 13: asciiDigit = 65; break; // 'A' + case 14: asciiDigit = 66; break; // 'B' + case 15: asciiDigit = 67; break; // 'C' + default: + asciiDigit = 32; // Invalid DTMF code + break; + } + + return asciiDigit; + } + + /** This function shall be called to get the number of voicemails. + * @hide + */ + public int getNumOfVoicemails() { + return mBearerData.numberOfMessages; + } + + /** + * Returns a byte array that can be use to uniquely identify a received SMS message. + * C.S0015-B 4.3.1.6 Unique Message Identification. + * + * @return byte array uniquely identifying the message. + * @hide + */ + public byte[] getIncomingSmsFingerprint() { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + output.write(mEnvelope.serviceCategory); + output.write(mEnvelope.teleService); + output.write(mEnvelope.origAddress.origBytes, 0, mEnvelope.origAddress.origBytes.length); + output.write(mEnvelope.bearerData, 0, mEnvelope.bearerData.length); + output.write(mEnvelope.origSubaddress.origBytes, 0, + mEnvelope.origSubaddress.origBytes.length); + + return output.toByteArray(); + } + + /** + * Returns the list of service category program data, if present. + * @return a list of CdmaSmsCbProgramData objects, or null if not present + * @hide + */ + public ArrayList<CdmaSmsCbProgramData> getSmsCbProgramData() { + return mBearerData.serviceCategoryProgramData; + } +} diff --git a/telephony/java/com/android/internal/telephony/cdma/UserData.java b/telephony/java/com/android/internal/telephony/cdma/UserData.java new file mode 100644 index 000000000000..599c2b393a6b --- /dev/null +++ b/telephony/java/com/android/internal/telephony/cdma/UserData.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma.sms; + +import android.util.SparseIntArray; + +import com.android.internal.telephony.SmsHeader; +import com.android.internal.util.HexDump; + +public class UserData { + + /** + * User data encoding types. + * (See 3GPP2 C.R1001-F, v1.0, table 9.1-1) + */ + public static final int ENCODING_OCTET = 0x00; + public static final int ENCODING_IS91_EXTENDED_PROTOCOL = 0x01; + public static final int ENCODING_7BIT_ASCII = 0x02; + public static final int ENCODING_IA5 = 0x03; + public static final int ENCODING_UNICODE_16 = 0x04; + public static final int ENCODING_SHIFT_JIS = 0x05; + public static final int ENCODING_KOREAN = 0x06; + public static final int ENCODING_LATIN_HEBREW = 0x07; + public static final int ENCODING_LATIN = 0x08; + public static final int ENCODING_GSM_7BIT_ALPHABET = 0x09; + public static final int ENCODING_GSM_DCS = 0x0A; + + /** + * IS-91 message types. + * (See TIA/EIS/IS-91-A-ENGL 1999, table 3.7.1.1-3) + */ + public static final int IS91_MSG_TYPE_VOICEMAIL_STATUS = 0x82; + public static final int IS91_MSG_TYPE_SHORT_MESSAGE_FULL = 0x83; + public static final int IS91_MSG_TYPE_CLI = 0x84; + public static final int IS91_MSG_TYPE_SHORT_MESSAGE = 0x85; + + /** + * US ASCII character mapping table. + * + * This table contains only the printable ASCII characters, with a + * 0x20 offset, meaning that the ASCII SPACE character is at index + * 0, with the resulting code of 0x20. + * + * Note this mapping is also equivalent to that used by both the + * IA5 and the IS-91 encodings. For the former this is defined + * using CCITT Rec. T.50 Tables 1 and 3. For the latter IS 637 B, + * Table 4.3.1.4.1-1 -- and note the encoding uses only 6 bits, + * and hence only maps entries up to the '_' character. + * + */ + public static final char[] ASCII_MAP = { + ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', + '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', + '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~'}; + + /** + * Character to use when forced to encode otherwise unencodable + * characters, meaning those not in the respective ASCII or GSM + * 7-bit encoding tables. Current choice is SPACE, which is 0x20 + * in both the GSM-7bit and ASCII-7bit encodings. + */ + static final byte UNENCODABLE_7_BIT_CHAR = 0x20; + + /** + * Only elements between these indices in the ASCII table are printable. + */ + public static final int PRINTABLE_ASCII_MIN_INDEX = 0x20; + public static final int ASCII_NL_INDEX = 0x0A; + public static final int ASCII_CR_INDEX = 0x0D; + public static final SparseIntArray charToAscii = new SparseIntArray(); + static { + for (int i = 0; i < ASCII_MAP.length; i++) { + charToAscii.put(ASCII_MAP[i], PRINTABLE_ASCII_MIN_INDEX + i); + } + charToAscii.put('\n', ASCII_NL_INDEX); + charToAscii.put('\r', ASCII_CR_INDEX); + } + + /* + * TODO(cleanup): Move this very generic functionality somewhere + * more general. + */ + /** + * Given a string generate a corresponding ASCII-encoded byte + * array, but limited to printable characters. If the input + * contains unprintable characters, return null. + */ + public static byte[] stringToAscii(String str) { + int len = str.length(); + byte[] result = new byte[len]; + for (int i = 0; i < len; i++) { + int charCode = charToAscii.get(str.charAt(i), -1); + if (charCode == -1) return null; + result[i] = (byte)charCode; + } + return result; + } + + /** + * Mapping for ASCII values less than 32 are flow control signals + * and not used here. + */ + public static final int ASCII_MAP_BASE_INDEX = 0x20; + public static final int ASCII_MAP_MAX_INDEX = ASCII_MAP_BASE_INDEX + ASCII_MAP.length - 1; + + /** + * Contains the data header of the user data + */ + public SmsHeader userDataHeader; + + /** + * Contains the data encoding type for the SMS message + */ + public int msgEncoding; + public boolean msgEncodingSet = false; + + public int msgType; + + /** + * Number of invalid bits in the last byte of data. + */ + public int paddingBits; + + public int numFields; + + /** + * Contains the user data of a SMS message + * (See 3GPP2 C.S0015-B, v2, 4.5.2) + */ + public byte[] payload; + public String payloadStr; + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("UserData "); + builder.append("{ msgEncoding=" + (msgEncodingSet ? msgEncoding : "unset")); + builder.append(", msgType=" + msgType); + builder.append(", paddingBits=" + paddingBits); + builder.append(", numFields=" + numFields); + builder.append(", userDataHeader=" + userDataHeader); + builder.append(", payload='" + HexDump.toHexString(payload) + "'"); + builder.append(", payloadStr='" + payloadStr + "'"); + builder.append(" }"); + return builder.toString(); + } + +} diff --git a/telephony/java/com/android/internal/telephony/cdma/package.html b/telephony/java/com/android/internal/telephony/cdma/package.html new file mode 100644 index 000000000000..b2bc7364748f --- /dev/null +++ b/telephony/java/com/android/internal/telephony/cdma/package.html @@ -0,0 +1,6 @@ +<HTML> +<BODY> +Provides CDMA-specific features for text/data/PDU SMS messages +@hide +</BODY> +</HTML> diff --git a/telephony/java/com/android/internal/telephony/gsm/GsmSmsAddress.java b/telephony/java/com/android/internal/telephony/gsm/GsmSmsAddress.java new file mode 100644 index 000000000000..2fbf7ed5648c --- /dev/null +++ b/telephony/java/com/android/internal/telephony/gsm/GsmSmsAddress.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2006 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 com.android.internal.telephony.gsm; + +import android.telephony.PhoneNumberUtils; +import java.text.ParseException; +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.SmsAddress; + +public class GsmSmsAddress extends SmsAddress { + + static final int OFFSET_ADDRESS_LENGTH = 0; + + static final int OFFSET_TOA = 1; + + static final int OFFSET_ADDRESS_VALUE = 2; + + /** + * New GsmSmsAddress from TS 23.040 9.1.2.5 Address Field + * + * @param offset the offset of the Address-Length byte + * @param length the length in bytes rounded up, e.g. "2 + + * (addressLength + 1) / 2" + * @throws ParseException + */ + + public GsmSmsAddress(byte[] data, int offset, int length) throws ParseException { + origBytes = new byte[length]; + System.arraycopy(data, offset, origBytes, 0, length); + + // addressLength is the count of semi-octets, not bytes + int addressLength = origBytes[OFFSET_ADDRESS_LENGTH] & 0xff; + + int toa = origBytes[OFFSET_TOA] & 0xff; + ton = 0x7 & (toa >> 4); + + // TOA must have its high bit set + if ((toa & 0x80) != 0x80) { + throw new ParseException("Invalid TOA - high bit must be set. toa = " + toa, + offset + OFFSET_TOA); + } + + if (isAlphanumeric()) { + // An alphanumeric address + int countSeptets = addressLength * 4 / 7; + + address = GsmAlphabet.gsm7BitPackedToString(origBytes, + OFFSET_ADDRESS_VALUE, countSeptets); + } else { + // TS 23.040 9.1.2.5 says + // that "the MS shall interpret reserved values as 'Unknown' + // but shall store them exactly as received" + + byte lastByte = origBytes[length - 1]; + + if ((addressLength & 1) == 1) { + // Make sure the final unused BCD digit is 0xf + origBytes[length - 1] |= 0xf0; + } + address = PhoneNumberUtils.calledPartyBCDToString(origBytes, + OFFSET_TOA, length - OFFSET_TOA); + + // And restore origBytes + origBytes[length - 1] = lastByte; + } + } + + @Override + public String getAddressString() { + return address; + } + + /** + * Returns true if this is an alphanumeric address + */ + @Override + public boolean isAlphanumeric() { + return ton == TON_ALPHANUMERIC; + } + + @Override + public boolean isNetworkSpecific() { + return ton == TON_NETWORK; + } + + /** + * Returns true of this is a valid CPHS voice message waiting indicator + * address + */ + public boolean isCphsVoiceMessageIndicatorAddress() { + // CPHS-style MWI message + // See CPHS 4.7 B.4.2.1 + // + // Basically: + // + // - Originating address should be 4 bytes long and alphanumeric + // - Decode will result with two chars: + // - Char 1 + // 76543210 + // ^ set/clear indicator (0 = clear) + // ^^^ type of indicator (000 = voice) + // ^^^^ must be equal to 0001 + // - Char 2: + // 76543210 + // ^ line number (0 = line 1) + // ^^^^^^^ set to 0 + // + // Remember, since the alpha address is stored in 7-bit compact form, + // the "line number" is really the top bit of the first address value + // byte + + return (origBytes[OFFSET_ADDRESS_LENGTH] & 0xff) == 4 + && isAlphanumeric() && (origBytes[OFFSET_TOA] & 0x0f) == 0; + } + + /** + * Returns true if this is a valid CPHS voice message waiting indicator + * address indicating a "set" of "indicator 1" of type "voice message + * waiting" + */ + public boolean isCphsVoiceMessageSet() { + // 0x11 means "set" "voice message waiting" "indicator 1" + return isCphsVoiceMessageIndicatorAddress() + && (origBytes[OFFSET_ADDRESS_VALUE] & 0xff) == 0x11; + + } + + /** + * Returns true if this is a valid CPHS voice message waiting indicator + * address indicating a "clear" of "indicator 1" of type "voice message + * waiting" + */ + public boolean isCphsVoiceMessageClear() { + // 0x10 means "clear" "voice message waiting" "indicator 1" + return isCphsVoiceMessageIndicatorAddress() + && (origBytes[OFFSET_ADDRESS_VALUE] & 0xff) == 0x10; + + } +} diff --git a/telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java b/telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java new file mode 100644 index 000000000000..15ed81098813 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2012 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 com.android.internal.telephony.gsm; + +import android.telephony.SmsCbLocation; +import android.telephony.SmsCbMessage; +import android.util.Pair; + +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.SmsConstants; + +import java.io.UnsupportedEncodingException; + +/** + * Parses a GSM or UMTS format SMS-CB message into an {@link SmsCbMessage} object. The class is + * public because {@link #createSmsCbMessage(SmsCbLocation, byte[][])} is used by some test cases. + */ +public class GsmSmsCbMessage { + + /** + * Languages in the 0000xxxx DCS group as defined in 3GPP TS 23.038, section 5. + */ + private static final String[] LANGUAGE_CODES_GROUP_0 = { + "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi", "no", "el", "tr", "hu", + "pl", null + }; + + /** + * Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5. + */ + private static final String[] LANGUAGE_CODES_GROUP_2 = { + "cs", "he", "ar", "ru", "is", null, null, null, null, null, null, null, null, null, + null, null + }; + + private static final char CARRIAGE_RETURN = 0x0d; + + private static final int PDU_BODY_PAGE_LENGTH = 82; + + /** Utility class with only static methods. */ + private GsmSmsCbMessage() { } + + /** + * Create a new SmsCbMessage object from a header object plus one or more received PDUs. + * + * @param pdus PDU bytes + */ + public static SmsCbMessage createSmsCbMessage(SmsCbHeader header, SmsCbLocation location, + byte[][] pdus) throws IllegalArgumentException { + if (header.isEtwsPrimaryNotification()) { + // ETSI TS 23.041 ETWS Primary Notification message + // ETWS primary message only contains 4 fields including serial number, + // message identifier, warning type, and warning security information. + // There is no field for the content/text. We hardcode "ETWS" in the + // text body so the user won't see an empty dialog without any text. + return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, + header.getGeographicalScope(), header.getSerialNumber(), + location, header.getServiceCategory(), + null, "ETWS", SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, + header.getEtwsInfo(), header.getCmasInfo()); + } else { + String language = null; + StringBuilder sb = new StringBuilder(); + for (byte[] pdu : pdus) { + Pair<String, String> p = parseBody(header, pdu); + language = p.first; + sb.append(p.second); + } + int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY + : SmsCbMessage.MESSAGE_PRIORITY_NORMAL; + + return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, + header.getGeographicalScope(), header.getSerialNumber(), location, + header.getServiceCategory(), language, sb.toString(), priority, + header.getEtwsInfo(), header.getCmasInfo()); + } + } + + /** + * Create a new SmsCbMessage object from one or more received PDUs. This is used by some + * CellBroadcastReceiver test cases, because SmsCbHeader is now package local. + * + * @param location the location (geographical scope) for the message + * @param pdus PDU bytes + */ + public static SmsCbMessage createSmsCbMessage(SmsCbLocation location, byte[][] pdus) + throws IllegalArgumentException { + SmsCbHeader header = new SmsCbHeader(pdus[0]); + return createSmsCbMessage(header, location, pdus); + } + + /** + * Parse and unpack the body text according to the encoding in the DCS. + * After completing successfully this method will have assigned the body + * text into mBody, and optionally the language code into mLanguage + * + * @param header the message header to use + * @param pdu the PDU to decode + * @return a Pair of Strings containing the language and body of the message + */ + private static Pair<String, String> parseBody(SmsCbHeader header, byte[] pdu) { + int encoding; + String language = null; + boolean hasLanguageIndicator = false; + int dataCodingScheme = header.getDataCodingScheme(); + + // Extract encoding and language from DCS, as defined in 3gpp TS 23.038, + // section 5. + switch ((dataCodingScheme & 0xf0) >> 4) { + case 0x00: + encoding = SmsConstants.ENCODING_7BIT; + language = LANGUAGE_CODES_GROUP_0[dataCodingScheme & 0x0f]; + break; + + case 0x01: + hasLanguageIndicator = true; + if ((dataCodingScheme & 0x0f) == 0x01) { + encoding = SmsConstants.ENCODING_16BIT; + } else { + encoding = SmsConstants.ENCODING_7BIT; + } + break; + + case 0x02: + encoding = SmsConstants.ENCODING_7BIT; + language = LANGUAGE_CODES_GROUP_2[dataCodingScheme & 0x0f]; + break; + + case 0x03: + encoding = SmsConstants.ENCODING_7BIT; + break; + + case 0x04: + case 0x05: + switch ((dataCodingScheme & 0x0c) >> 2) { + case 0x01: + encoding = SmsConstants.ENCODING_8BIT; + break; + + case 0x02: + encoding = SmsConstants.ENCODING_16BIT; + break; + + case 0x00: + default: + encoding = SmsConstants.ENCODING_7BIT; + break; + } + break; + + case 0x06: + case 0x07: + // Compression not supported + case 0x09: + // UDH structure not supported + case 0x0e: + // Defined by the WAP forum not supported + throw new IllegalArgumentException("Unsupported GSM dataCodingScheme " + + dataCodingScheme); + + case 0x0f: + if (((dataCodingScheme & 0x04) >> 2) == 0x01) { + encoding = SmsConstants.ENCODING_8BIT; + } else { + encoding = SmsConstants.ENCODING_7BIT; + } + break; + + default: + // Reserved values are to be treated as 7-bit + encoding = SmsConstants.ENCODING_7BIT; + break; + } + + if (header.isUmtsFormat()) { + // Payload may contain multiple pages + int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; + + if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) + * nrPages) { + throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match " + + nrPages + " pages"); + } + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < nrPages; i++) { + // Each page is 82 bytes followed by a length octet indicating + // the number of useful octets within those 82 + int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i; + int length = pdu[offset + PDU_BODY_PAGE_LENGTH]; + + if (length > PDU_BODY_PAGE_LENGTH) { + throw new IllegalArgumentException("Page length " + length + + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH); + } + + Pair<String, String> p = unpackBody(pdu, encoding, offset, length, + hasLanguageIndicator, language); + language = p.first; + sb.append(p.second); + } + return new Pair<String, String>(language, sb.toString()); + } else { + // Payload is one single page + int offset = SmsCbHeader.PDU_HEADER_LENGTH; + int length = pdu.length - offset; + + return unpackBody(pdu, encoding, offset, length, hasLanguageIndicator, language); + } + } + + /** + * Unpack body text from the pdu using the given encoding, position and + * length within the pdu + * + * @param pdu The pdu + * @param encoding The encoding, as derived from the DCS + * @param offset Position of the first byte to unpack + * @param length Number of bytes to unpack + * @param hasLanguageIndicator true if the body text is preceded by a + * language indicator. If so, this method will as a side-effect + * assign the extracted language code into mLanguage + * @param language the language to return if hasLanguageIndicator is false + * @return a Pair of Strings containing the language and body of the message + */ + private static Pair<String, String> unpackBody(byte[] pdu, int encoding, int offset, int length, + boolean hasLanguageIndicator, String language) { + String body = null; + + switch (encoding) { + case SmsConstants.ENCODING_7BIT: + body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7); + + if (hasLanguageIndicator && body != null && body.length() > 2) { + // Language is two GSM characters followed by a CR. + // The actual body text is offset by 3 characters. + language = body.substring(0, 2); + body = body.substring(3); + } + break; + + case SmsConstants.ENCODING_16BIT: + if (hasLanguageIndicator && pdu.length >= offset + 2) { + // Language is two GSM characters. + // The actual body text is offset by 2 bytes. + language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2); + offset += 2; + length -= 2; + } + + try { + body = new String(pdu, offset, (length & 0xfffe), "utf-16"); + } catch (UnsupportedEncodingException e) { + // Apparently it wasn't valid UTF-16. + throw new IllegalArgumentException("Error decoding UTF-16 message", e); + } + break; + + default: + break; + } + + if (body != null) { + // Remove trailing carriage return + for (int i = body.length() - 1; i >= 0; i--) { + if (body.charAt(i) != CARRIAGE_RETURN) { + body = body.substring(0, i + 1); + break; + } + } + } else { + body = ""; + } + + return new Pair<String, String>(language, body); + } +} diff --git a/telephony/java/com/android/internal/telephony/gsm/SmsBroadcastConfigInfo.java b/telephony/java/com/android/internal/telephony/gsm/SmsBroadcastConfigInfo.java new file mode 100644 index 000000000000..f4f40361fb69 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/gsm/SmsBroadcastConfigInfo.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2009 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 com.android.internal.telephony.gsm; + +/** + * SmsBroadcastConfigInfo defines one configuration of Cell Broadcast + * Message (CBM) to be received by the ME + * + * fromServiceId - toServiceId defines a range of CBM message identifiers + * whose value is 0x0000 - 0xFFFF as defined in TS 23.041 9.4.1.2.2 for GMS + * and 9.4.4.2.2 for UMTS. All other values can be treated as empty + * CBM message ID. + * + * fromCodeScheme - toCodeScheme defines a range of CBM data coding schemes + * whose value is 0x00 - 0xFF as defined in TS 23.041 9.4.1.2.3 for GMS + * and 9.4.4.2.3 for UMTS. + * All other values can be treated as empty CBM data coding scheme. + * + * selected false means message types specified in {@code <fromServiceId, toServiceId>} + * and {@code <fromCodeScheme, toCodeScheme>} are not accepted, while true means accepted. + * + */ +public final class SmsBroadcastConfigInfo { + private int mFromServiceId; + private int mToServiceId; + private int mFromCodeScheme; + private int mToCodeScheme; + private boolean mSelected; + + /** + * Initialize the object from rssi and cid. + */ + public SmsBroadcastConfigInfo(int fromId, int toId, int fromScheme, + int toScheme, boolean selected) { + mFromServiceId = fromId; + mToServiceId = toId; + mFromCodeScheme = fromScheme; + mToCodeScheme = toScheme; + mSelected = selected; + } + + /** + * @param fromServiceId the fromServiceId to set + */ + public void setFromServiceId(int fromServiceId) { + mFromServiceId = fromServiceId; + } + + /** + * @return the fromServiceId + */ + public int getFromServiceId() { + return mFromServiceId; + } + + /** + * @param toServiceId the toServiceId to set + */ + public void setToServiceId(int toServiceId) { + mToServiceId = toServiceId; + } + + /** + * @return the toServiceId + */ + public int getToServiceId() { + return mToServiceId; + } + + /** + * @param fromCodeScheme the fromCodeScheme to set + */ + public void setFromCodeScheme(int fromCodeScheme) { + mFromCodeScheme = fromCodeScheme; + } + + /** + * @return the fromCodeScheme + */ + public int getFromCodeScheme() { + return mFromCodeScheme; + } + + /** + * @param toCodeScheme the toCodeScheme to set + */ + public void setToCodeScheme(int toCodeScheme) { + mToCodeScheme = toCodeScheme; + } + + /** + * @return the toCodeScheme + */ + public int getToCodeScheme() { + return mToCodeScheme; + } + + /** + * @param selected the selected to set + */ + public void setSelected(boolean selected) { + mSelected = selected; + } + + /** + * @return the selected + */ + public boolean isSelected() { + return mSelected; + } + + @Override + public String toString() { + return "SmsBroadcastConfigInfo: Id [" + + mFromServiceId + ',' + mToServiceId + "] Code [" + + mFromCodeScheme + ',' + mToCodeScheme + "] " + + (mSelected ? "ENABLED" : "DISABLED"); + } +} diff --git a/telephony/java/com/android/internal/telephony/gsm/SmsCbConstants.java b/telephony/java/com/android/internal/telephony/gsm/SmsCbConstants.java new file mode 100644 index 000000000000..bce5680b2863 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/gsm/SmsCbConstants.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2012 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 com.android.internal.telephony.gsm; + +/** + * Constants used in SMS Cell Broadcast messages (see 3GPP TS 23.041). This class is used by the + * boot-time broadcast channel enable and database upgrade code in CellBroadcastReceiver, so it + * is public, but should be avoided in favor of the radio technology independent constants in + * {@link android.telephony.SmsCbMessage}, {@link android.telephony.SmsCbEtwsInfo}, and + * {@link android.telephony.SmsCbCmasInfo} classes. + * + * {@hide} + */ +public class SmsCbConstants { + + /** Private constructor for utility class. */ + private SmsCbConstants() { } + + /** Channel 50 required by Brazil. ID 0~999 is allocated by GSMA */ + public static final int MESSAGE_ID_GSMA_ALLOCATED_CHANNEL_50 + = 0x0032; + + /** Channel 911 required by Taiwan NCC. ID 0~999 is allocated by GSMA */ + public static final int MESSAGE_ID_GSMA_ALLOCATED_CHANNEL_911 + = 0x038F; // 911 + + /** Channel 919 required by Taiwan NCC and Israel. ID 0~999 is allocated by GSMA */ + public static final int MESSAGE_ID_GSMA_ALLOCATED_CHANNEL_919 + = 0x0397; // 919 + + /** Channel 928 required by Israel. ID 0~999 is allocated by GSMA */ + public static final int MESSAGE_ID_GSMA_ALLOCATED_CHANNEL_928 + = 0x03A0; // 928 + + /** Start of PWS Message Identifier range (includes ETWS and CMAS). */ + public static final int MESSAGE_ID_PWS_FIRST_IDENTIFIER + = 0x1100; // 4352 + + /** Bitmask for messages of ETWS type (including future extensions). */ + public static final int MESSAGE_ID_ETWS_TYPE_MASK + = 0xFFF8; + + /** Value for messages of ETWS type after applying {@link #MESSAGE_ID_ETWS_TYPE_MASK}. */ + public static final int MESSAGE_ID_ETWS_TYPE + = 0x1100; // 4352 + + /** ETWS Message Identifier for earthquake warning message. */ + public static final int MESSAGE_ID_ETWS_EARTHQUAKE_WARNING + = 0x1100; // 4352 + + /** ETWS Message Identifier for tsunami warning message. */ + public static final int MESSAGE_ID_ETWS_TSUNAMI_WARNING + = 0x1101; // 4353 + + /** ETWS Message Identifier for earthquake and tsunami combined warning message. */ + public static final int MESSAGE_ID_ETWS_EARTHQUAKE_AND_TSUNAMI_WARNING + = 0x1102; // 4354 + + /** ETWS Message Identifier for test message. */ + public static final int MESSAGE_ID_ETWS_TEST_MESSAGE + = 0x1103; // 4355 + + /** ETWS Message Identifier for messages related to other emergency types. */ + public static final int MESSAGE_ID_ETWS_OTHER_EMERGENCY_TYPE + = 0x1104; // 4356 + + /** Start of CMAS Message Identifier range. */ + public static final int MESSAGE_ID_CMAS_FIRST_IDENTIFIER + = 0x1112; // 4370 + + /** CMAS Message Identifier for Presidential Level alerts. */ + public static final int MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL + = 0x1112; // 4370 + + /** CMAS Message Identifier for Extreme alerts, Urgency=Immediate, Certainty=Observed. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED + = 0x1113; // 4371 + + /** CMAS Message Identifier for Extreme alerts, Urgency=Immediate, Certainty=Likely. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY + = 0x1114; // 4372 + + /** CMAS Message Identifier for Extreme alerts, Urgency=Expected, Certainty=Observed. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED + = 0x1115; // 4373 + + /** CMAS Message Identifier for Extreme alerts, Urgency=Expected, Certainty=Likely. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY + = 0x1116; // 4374 + + /** CMAS Message Identifier for Severe alerts, Urgency=Immediate, Certainty=Observed. */ + public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED + = 0x1117; // 4375 + + /** CMAS Message Identifier for Severe alerts, Urgency=Immediate, Certainty=Likely. */ + public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY + = 0x1118; // 4376 + + /** CMAS Message Identifier for Severe alerts, Urgency=Expected, Certainty=Observed. */ + public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED + = 0x1119; // 4377 + + /** CMAS Message Identifier for Severe alerts, Urgency=Expected, Certainty=Likely. */ + public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY + = 0x111A; // 4378 + + /** CMAS Message Identifier for Child Abduction Emergency (Amber Alert). */ + public static final int MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY + = 0x111B; // 4379 + + /** CMAS Message Identifier for the Required Monthly Test. */ + public static final int MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST + = 0x111C; // 4380 + + /** CMAS Message Identifier for CMAS Exercise. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXERCISE + = 0x111D; // 4381 + + /** CMAS Message Identifier for operator defined use. */ + public static final int MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE + = 0x111E; // 4382 + + /** CMAS Message Identifier for Presidential Level alerts for additional languages + * for additional languages. */ + public static final int MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL_LANGUAGE + = 0x111F; // 4383 + + /** CMAS Message Identifier for Extreme alerts, Urgency=Immediate, Certainty=Observed + * for additional languages. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE + = 0x1120; // 4384 + + /** CMAS Message Identifier for Extreme alerts, Urgency=Immediate, Certainty=Likely + * for additional languages. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE + = 0x1121; // 4385 + + /** CMAS Message Identifier for Extreme alerts, Urgency=Expected, Certainty=Observed + * for additional languages. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE + = 0x1122; // 4386 + + /** CMAS Message Identifier for Extreme alerts, Urgency=Expected, Certainty=Likely + * for additional languages. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE + = 0x1123; // 4387 + + /** CMAS Message Identifier for Severe alerts, Urgency=Immediate, Certainty=Observed + * for additional languages. */ + public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE + = 0x1124; // 4388 + + /** CMAS Message Identifier for Severe alerts, Urgency=Immediate, Certainty=Likely + * for additional languages.*/ + public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE + = 0x1125; // 4389 + + /** CMAS Message Identifier for Severe alerts, Urgency=Expected, Certainty=Observed + * for additional languages. */ + public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE + = 0x1126; // 4390 + + /** CMAS Message Identifier for Severe alerts, Urgency=Expected, Certainty=Likely + * for additional languages.*/ + public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE + = 0x1127; // 4391 + + /** CMAS Message Identifier for Child Abduction Emergency (Amber Alert) + * for additional languages. */ + public static final int MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY_LANGUAGE + = 0x1128; // 4392 + + /** CMAS Message Identifier for the Required Monthly Test + * for additional languages. */ + public static final int MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST_LANGUAGE + = 0x1129; // 4393 + + /** CMAS Message Identifier for CMAS Exercise + * for additional languages. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXERCISE_LANGUAGE + = 0x112A; // 4394 + + /** CMAS Message Identifier for operator defined use + * for additional languages. */ + public static final int MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE_LANGUAGE + = 0x112B; // 4395 + + /** End of CMAS Message Identifier range (including future extensions). */ + public static final int MESSAGE_ID_CMAS_LAST_IDENTIFIER + = 0x112F; // 4399 + + /** End of PWS Message Identifier range (includes ETWS, CMAS, and future extensions). */ + public static final int MESSAGE_ID_PWS_LAST_IDENTIFIER + = 0x18FF; // 6399 + + /** ETWS serial number flag to activate the popup display. */ + public static final int SERIAL_NUMBER_ETWS_ACTIVATE_POPUP + = 0x1000; // 4096 + + /** ETWS serial number flag to activate the emergency user alert. */ + public static final int SERIAL_NUMBER_ETWS_EMERGENCY_USER_ALERT + = 0x2000; // 8192 +} diff --git a/telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java b/telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java new file mode 100644 index 000000000000..d267ad204f57 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java @@ -0,0 +1,450 @@ +/* + * Copyright (C) 2010 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 com.android.internal.telephony.gsm; + +import android.telephony.SmsCbCmasInfo; +import android.telephony.SmsCbEtwsInfo; + +import java.util.Arrays; + +/** + * Parses a 3GPP TS 23.041 cell broadcast message header. This class is public for use by + * CellBroadcastReceiver test cases, but should not be used by applications. + * + * All relevant header information is now sent as a Parcelable + * {@link android.telephony.SmsCbMessage} object in the "message" extra of the + * {@link android.provider.Telephony.Sms.Intents#SMS_CB_RECEIVED_ACTION} or + * {@link android.provider.Telephony.Sms.Intents#SMS_EMERGENCY_CB_RECEIVED_ACTION} intent. + * The raw PDU is no longer sent to SMS CB applications. + */ +public class SmsCbHeader { + + /** + * Length of SMS-CB header + */ + static final int PDU_HEADER_LENGTH = 6; + + /** + * GSM pdu format, as defined in 3gpp TS 23.041, section 9.4.1 + */ + static final int FORMAT_GSM = 1; + + /** + * UMTS pdu format, as defined in 3gpp TS 23.041, section 9.4.2 + */ + static final int FORMAT_UMTS = 2; + + /** + * GSM pdu format, as defined in 3gpp TS 23.041, section 9.4.1.3 + */ + static final int FORMAT_ETWS_PRIMARY = 3; + + /** + * Message type value as defined in 3gpp TS 25.324, section 11.1. + */ + private static final int MESSAGE_TYPE_CBS_MESSAGE = 1; + + /** + * Length of GSM pdus + */ + private static final int PDU_LENGTH_GSM = 88; + + /** + * Maximum length of ETWS primary message GSM pdus + */ + private static final int PDU_LENGTH_ETWS = 56; + + private final int mGeographicalScope; + + /** The serial number combines geographical scope, message code, and update number. */ + private final int mSerialNumber; + + /** The Message Identifier in 3GPP is the same as the Service Category in CDMA. */ + private final int mMessageIdentifier; + + private final int mDataCodingScheme; + + private final int mPageIndex; + + private final int mNrOfPages; + + private final int mFormat; + + /** ETWS warning notification info. */ + private final SmsCbEtwsInfo mEtwsInfo; + + /** CMAS warning notification info. */ + private final SmsCbCmasInfo mCmasInfo; + + public SmsCbHeader(byte[] pdu) throws IllegalArgumentException { + if (pdu == null || pdu.length < PDU_HEADER_LENGTH) { + throw new IllegalArgumentException("Illegal PDU"); + } + + if (pdu.length <= PDU_LENGTH_GSM) { + // can be ETWS or GSM format. + // Per TS23.041 9.4.1.2 and 9.4.1.3.2, GSM and ETWS format both + // contain serial number which contains GS, Message Code, and Update Number + // per 9.4.1.2.1, and message identifier in same octets + mGeographicalScope = (pdu[0] & 0xc0) >>> 6; + mSerialNumber = ((pdu[0] & 0xff) << 8) | (pdu[1] & 0xff); + mMessageIdentifier = ((pdu[2] & 0xff) << 8) | (pdu[3] & 0xff); + if (isEtwsMessage() && pdu.length <= PDU_LENGTH_ETWS) { + mFormat = FORMAT_ETWS_PRIMARY; + mDataCodingScheme = -1; + mPageIndex = -1; + mNrOfPages = -1; + boolean emergencyUserAlert = (pdu[4] & 0x1) != 0; + boolean activatePopup = (pdu[5] & 0x80) != 0; + int warningType = (pdu[4] & 0xfe) >>> 1; + byte[] warningSecurityInfo; + // copy the Warning-Security-Information, if present + if (pdu.length > PDU_HEADER_LENGTH) { + warningSecurityInfo = Arrays.copyOfRange(pdu, 6, pdu.length); + } else { + warningSecurityInfo = null; + } + mEtwsInfo = new SmsCbEtwsInfo(warningType, emergencyUserAlert, activatePopup, + true, warningSecurityInfo); + mCmasInfo = null; + return; // skip the ETWS/CMAS initialization code for regular notifications + } else { + // GSM pdus are no more than 88 bytes + mFormat = FORMAT_GSM; + mDataCodingScheme = pdu[4] & 0xff; + + // Check for invalid page parameter + int pageIndex = (pdu[5] & 0xf0) >>> 4; + int nrOfPages = pdu[5] & 0x0f; + + if (pageIndex == 0 || nrOfPages == 0 || pageIndex > nrOfPages) { + pageIndex = 1; + nrOfPages = 1; + } + + mPageIndex = pageIndex; + mNrOfPages = nrOfPages; + } + } else { + // UMTS pdus are always at least 90 bytes since the payload includes + // a number-of-pages octet and also one length octet per page + mFormat = FORMAT_UMTS; + + int messageType = pdu[0]; + + if (messageType != MESSAGE_TYPE_CBS_MESSAGE) { + throw new IllegalArgumentException("Unsupported message type " + messageType); + } + + mMessageIdentifier = ((pdu[1] & 0xff) << 8) | pdu[2] & 0xff; + mGeographicalScope = (pdu[3] & 0xc0) >>> 6; + mSerialNumber = ((pdu[3] & 0xff) << 8) | (pdu[4] & 0xff); + mDataCodingScheme = pdu[5] & 0xff; + + // We will always consider a UMTS message as having one single page + // since there's only one instance of the header, even though the + // actual payload may contain several pages. + mPageIndex = 1; + mNrOfPages = 1; + } + + if (isEtwsMessage()) { + boolean emergencyUserAlert = isEtwsEmergencyUserAlert(); + boolean activatePopup = isEtwsPopupAlert(); + int warningType = getEtwsWarningType(); + mEtwsInfo = new SmsCbEtwsInfo(warningType, emergencyUserAlert, activatePopup, + false, null); + mCmasInfo = null; + } else if (isCmasMessage()) { + int messageClass = getCmasMessageClass(); + int severity = getCmasSeverity(); + int urgency = getCmasUrgency(); + int certainty = getCmasCertainty(); + mEtwsInfo = null; + mCmasInfo = new SmsCbCmasInfo(messageClass, SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN, + SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, severity, urgency, certainty); + } else { + mEtwsInfo = null; + mCmasInfo = null; + } + } + + int getGeographicalScope() { + return mGeographicalScope; + } + + int getSerialNumber() { + return mSerialNumber; + } + + int getServiceCategory() { + return mMessageIdentifier; + } + + int getDataCodingScheme() { + return mDataCodingScheme; + } + + int getPageIndex() { + return mPageIndex; + } + + int getNumberOfPages() { + return mNrOfPages; + } + + SmsCbEtwsInfo getEtwsInfo() { + return mEtwsInfo; + } + + SmsCbCmasInfo getCmasInfo() { + return mCmasInfo; + } + + /** + * Return whether this broadcast is an emergency (PWS) message type. + * @return true if this message is emergency type; false otherwise + */ + boolean isEmergencyMessage() { + return mMessageIdentifier >= SmsCbConstants.MESSAGE_ID_PWS_FIRST_IDENTIFIER + && mMessageIdentifier <= SmsCbConstants.MESSAGE_ID_PWS_LAST_IDENTIFIER; + } + + /** + * Return whether this broadcast is an ETWS emergency message type. + * @return true if this message is ETWS emergency type; false otherwise + */ + private boolean isEtwsMessage() { + return (mMessageIdentifier & SmsCbConstants.MESSAGE_ID_ETWS_TYPE_MASK) + == SmsCbConstants.MESSAGE_ID_ETWS_TYPE; + } + + /** + * Return whether this broadcast is an ETWS primary notification. + * @return true if this message is an ETWS primary notification; false otherwise + */ + boolean isEtwsPrimaryNotification() { + return mFormat == FORMAT_ETWS_PRIMARY; + } + + /** + * Return whether this broadcast is in UMTS format. + * @return true if this message is in UMTS format; false otherwise + */ + boolean isUmtsFormat() { + return mFormat == FORMAT_UMTS; + } + + /** + * Return whether this message is a CMAS emergency message type. + * @return true if this message is CMAS emergency type; false otherwise + */ + private boolean isCmasMessage() { + return mMessageIdentifier >= SmsCbConstants.MESSAGE_ID_CMAS_FIRST_IDENTIFIER + && mMessageIdentifier <= SmsCbConstants.MESSAGE_ID_CMAS_LAST_IDENTIFIER; + } + + /** + * Return whether the popup alert flag is set for an ETWS warning notification. + * This method assumes that the message ID has already been checked for ETWS type. + * + * @return true if the message code indicates a popup alert should be displayed + */ + private boolean isEtwsPopupAlert() { + return (mSerialNumber & SmsCbConstants.SERIAL_NUMBER_ETWS_ACTIVATE_POPUP) != 0; + } + + /** + * Return whether the emergency user alert flag is set for an ETWS warning notification. + * This method assumes that the message ID has already been checked for ETWS type. + * + * @return true if the message code indicates an emergency user alert + */ + private boolean isEtwsEmergencyUserAlert() { + return (mSerialNumber & SmsCbConstants.SERIAL_NUMBER_ETWS_EMERGENCY_USER_ALERT) != 0; + } + + /** + * Returns the warning type for an ETWS warning notification. + * This method assumes that the message ID has already been checked for ETWS type. + * + * @return the ETWS warning type defined in 3GPP TS 23.041 section 9.3.24 + */ + private int getEtwsWarningType() { + return mMessageIdentifier - SmsCbConstants.MESSAGE_ID_ETWS_EARTHQUAKE_WARNING; + } + + /** + * Returns the message class for a CMAS warning notification. + * This method assumes that the message ID has already been checked for CMAS type. + * @return the CMAS message class as defined in {@link SmsCbCmasInfo} + */ + private int getCmasMessageClass() { + switch (mMessageIdentifier) { + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL_LANGUAGE: + return SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE: + return SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE: + return SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY_LANGUAGE: + return SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST_LANGUAGE: + return SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXERCISE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXERCISE_LANGUAGE: + return SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE_LANGUAGE: + return SmsCbCmasInfo.CMAS_CLASS_OPERATOR_DEFINED_USE; + + default: + return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN; + } + } + + /** + * Returns the severity for a CMAS warning notification. This is only available for extreme + * and severe alerts, not for other types such as Presidential Level and AMBER alerts. + * This method assumes that the message ID has already been checked for CMAS type. + * @return the CMAS severity as defined in {@link SmsCbCmasInfo} + */ + private int getCmasSeverity() { + switch (mMessageIdentifier) { + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE: + return SmsCbCmasInfo.CMAS_SEVERITY_EXTREME; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE: + return SmsCbCmasInfo.CMAS_SEVERITY_SEVERE; + + default: + return SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN; + } + } + + /** + * Returns the urgency for a CMAS warning notification. This is only available for extreme + * and severe alerts, not for other types such as Presidential Level and AMBER alerts. + * This method assumes that the message ID has already been checked for CMAS type. + * @return the CMAS urgency as defined in {@link SmsCbCmasInfo} + */ + private int getCmasUrgency() { + switch (mMessageIdentifier) { + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE: + return SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE: + return SmsCbCmasInfo.CMAS_URGENCY_EXPECTED; + + default: + return SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN; + } + } + + /** + * Returns the certainty for a CMAS warning notification. This is only available for extreme + * and severe alerts, not for other types such as Presidential Level and AMBER alerts. + * This method assumes that the message ID has already been checked for CMAS type. + * @return the CMAS certainty as defined in {@link SmsCbCmasInfo} + */ + private int getCmasCertainty() { + switch (mMessageIdentifier) { + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE: + return SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE: + return SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY; + + default: + return SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN; + } + } + + @Override + public String toString() { + return "SmsCbHeader{GS=" + mGeographicalScope + ", serialNumber=0x" + + Integer.toHexString(mSerialNumber) + + ", messageIdentifier=0x" + Integer.toHexString(mMessageIdentifier) + + ", DCS=0x" + Integer.toHexString(mDataCodingScheme) + + ", page " + mPageIndex + " of " + mNrOfPages + '}'; + } +}
\ No newline at end of file diff --git a/telephony/java/com/android/internal/telephony/gsm/SmsMessage.java b/telephony/java/com/android/internal/telephony/gsm/SmsMessage.java new file mode 100644 index 000000000000..0d9bed489bc0 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/gsm/SmsMessage.java @@ -0,0 +1,1369 @@ +/* + * Copyright (C) 2006 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 com.android.internal.telephony.gsm; + +import android.telephony.PhoneNumberUtils; +import android.text.format.Time; +import android.telephony.Rlog; +import android.content.res.Resources; +import android.text.TextUtils; + +import com.android.internal.telephony.EncodeException; +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails; +import com.android.internal.telephony.uicc.IccUtils; +import com.android.internal.telephony.SmsHeader; +import com.android.internal.telephony.SmsMessageBase; +import com.android.internal.telephony.Sms7BitEncodingTranslator; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.text.ParseException; + +import static com.android.internal.telephony.SmsConstants.MessageClass; +import static com.android.internal.telephony.SmsConstants.ENCODING_UNKNOWN; +import static com.android.internal.telephony.SmsConstants.ENCODING_7BIT; +import static com.android.internal.telephony.SmsConstants.ENCODING_8BIT; +import static com.android.internal.telephony.SmsConstants.ENCODING_16BIT; +import static com.android.internal.telephony.SmsConstants.ENCODING_KSC5601; +import static com.android.internal.telephony.SmsConstants.MAX_USER_DATA_SEPTETS; +import static com.android.internal.telephony.SmsConstants.MAX_USER_DATA_BYTES; +import static com.android.internal.telephony.SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER; + +/** + * A Short Message Service message. + * + */ +public class SmsMessage extends SmsMessageBase { + static final String LOG_TAG = "SmsMessage"; + private static final boolean VDBG = false; + + private MessageClass messageClass; + + /** + * TP-Message-Type-Indicator + * 9.2.3 + */ + private int mMti; + + /** TP-Protocol-Identifier (TP-PID) */ + private int mProtocolIdentifier; + + // TP-Data-Coding-Scheme + // see TS 23.038 + private int mDataCodingScheme; + + // TP-Reply-Path + // e.g. 23.040 9.2.2.1 + private boolean mReplyPathPresent = false; + + /** The address of the receiver. */ + private GsmSmsAddress mRecipientAddress; + + /** + * TP-Status - status of a previously submitted SMS. + * This field applies to SMS-STATUS-REPORT messages. 0 indicates success; + * see TS 23.040, 9.2.3.15 for description of other possible values. + */ + private int mStatus; + + /** + * TP-Status - status of a previously submitted SMS. + * This field is true iff the message is a SMS-STATUS-REPORT message. + */ + private boolean mIsStatusReportMessage = false; + + private int mVoiceMailCount = 0; + + public static class SubmitPdu extends SubmitPduBase { + } + + /** + * Create an SmsMessage from a raw PDU. + */ + public static SmsMessage createFromPdu(byte[] pdu) { + try { + SmsMessage msg = new SmsMessage(); + msg.parsePdu(pdu); + return msg; + } catch (RuntimeException ex) { + Rlog.e(LOG_TAG, "SMS PDU parsing failed: ", ex); + return null; + } catch (OutOfMemoryError e) { + Rlog.e(LOG_TAG, "SMS PDU parsing failed with out of memory: ", e); + return null; + } + } + + /** + * 3GPP TS 23.040 9.2.3.9 specifies that Type Zero messages are indicated + * by TP_PID field set to value 0x40 + */ + public boolean isTypeZero() { + return (mProtocolIdentifier == 0x40); + } + + /** + * TS 27.005 3.4.1 lines[0] and lines[1] are the two lines read from the + * +CMT unsolicited response (PDU mode, of course) + * +CMT: [<alpha>],<length><CR><LF><pdu> + * + * Only public for debugging + * + * {@hide} + */ + public static SmsMessage newFromCMT(String[] lines) { + try { + SmsMessage msg = new SmsMessage(); + msg.parsePdu(IccUtils.hexStringToBytes(lines[1])); + return msg; + } catch (RuntimeException ex) { + Rlog.e(LOG_TAG, "SMS PDU parsing failed: ", ex); + return null; + } + } + + /** @hide */ + public static SmsMessage newFromCDS(String line) { + try { + SmsMessage msg = new SmsMessage(); + msg.parsePdu(IccUtils.hexStringToBytes(line)); + return msg; + } catch (RuntimeException ex) { + Rlog.e(LOG_TAG, "CDS SMS PDU parsing failed: ", ex); + return null; + } + } + + /** + * Create an SmsMessage from an SMS EF record. + * + * @param index Index of SMS record. This should be index in ArrayList + * returned by SmsManager.getAllMessagesFromSim + 1. + * @param data Record data. + * @return An SmsMessage representing the record. + * + * @hide + */ + public static SmsMessage createFromEfRecord(int index, byte[] data) { + try { + SmsMessage msg = new SmsMessage(); + + msg.mIndexOnIcc = index; + + // First byte is status: RECEIVED_READ, RECEIVED_UNREAD, STORED_SENT, + // or STORED_UNSENT + // See TS 51.011 10.5.3 + if ((data[0] & 1) == 0) { + Rlog.w(LOG_TAG, + "SMS parsing failed: Trying to parse a free record"); + return null; + } else { + msg.mStatusOnIcc = data[0] & 0x07; + } + + int size = data.length - 1; + + // Note: Data may include trailing FF's. That's OK; message + // should still parse correctly. + byte[] pdu = new byte[size]; + System.arraycopy(data, 1, pdu, 0, size); + msg.parsePdu(pdu); + return msg; + } catch (RuntimeException ex) { + Rlog.e(LOG_TAG, "SMS PDU parsing failed: ", ex); + return null; + } + } + + /** + * Get the TP-Layer-Length for the given SMS-SUBMIT PDU Basically, the + * length in bytes (not hex chars) less the SMSC header + */ + public static int getTPLayerLengthForPDU(String pdu) { + int len = pdu.length() / 2; + int smscLen = Integer.parseInt(pdu.substring(0, 2), 16); + + return len - smscLen - 1; + } + + /** + * Get an SMS-SUBMIT PDU for a destination address and a message + * + * @param scAddress Service Centre address. Null means use default. + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + * @hide + */ + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, String message, + boolean statusReportRequested, byte[] header) { + return getSubmitPdu(scAddress, destinationAddress, message, statusReportRequested, header, + ENCODING_UNKNOWN, 0, 0); + } + + + /** + * Get an SMS-SUBMIT PDU for a destination address and a message using the + * specified encoding. + * + * @param scAddress Service Centre address. Null means use default. + * @param encoding Encoding defined by constants in + * com.android.internal.telephony.SmsConstants.ENCODING_* + * @param languageTable + * @param languageShiftTable + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + * @hide + */ + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, String message, + boolean statusReportRequested, byte[] header, int encoding, + int languageTable, int languageShiftTable) { + + // Perform null parameter checks. + if (message == null || destinationAddress == null) { + return null; + } + + if (encoding == ENCODING_UNKNOWN) { + // Find the best encoding to use + TextEncodingDetails ted = calculateLength(message, false); + encoding = ted.codeUnitSize; + languageTable = ted.languageTable; + languageShiftTable = ted.languageShiftTable; + + if (encoding == ENCODING_7BIT && + (languageTable != 0 || languageShiftTable != 0)) { + if (header != null) { + SmsHeader smsHeader = SmsHeader.fromByteArray(header); + if (smsHeader.languageTable != languageTable + || smsHeader.languageShiftTable != languageShiftTable) { + Rlog.w(LOG_TAG, "Updating language table in SMS header: " + + smsHeader.languageTable + " -> " + languageTable + ", " + + smsHeader.languageShiftTable + " -> " + languageShiftTable); + smsHeader.languageTable = languageTable; + smsHeader.languageShiftTable = languageShiftTable; + header = SmsHeader.toByteArray(smsHeader); + } + } else { + SmsHeader smsHeader = new SmsHeader(); + smsHeader.languageTable = languageTable; + smsHeader.languageShiftTable = languageShiftTable; + header = SmsHeader.toByteArray(smsHeader); + } + } + } + + SubmitPdu ret = new SubmitPdu(); + // MTI = SMS-SUBMIT, UDHI = header != null + byte mtiByte = (byte)(0x01 | (header != null ? 0x40 : 0x00)); + ByteArrayOutputStream bo = getSubmitPduHead( + scAddress, destinationAddress, mtiByte, + statusReportRequested, ret); + + // User Data (and length) + byte[] userData; + try { + if (encoding == ENCODING_7BIT) { + userData = GsmAlphabet.stringToGsm7BitPackedWithHeader(message, header, + languageTable, languageShiftTable); + } else { //assume UCS-2 + try { + userData = encodeUCS2(message, header); + } catch(UnsupportedEncodingException uex) { + Rlog.e(LOG_TAG, + "Implausible UnsupportedEncodingException ", + uex); + return null; + } + } + } catch (EncodeException ex) { + // Encoding to the 7-bit alphabet failed. Let's see if we can + // send it as a UCS-2 encoded message + try { + userData = encodeUCS2(message, header); + encoding = ENCODING_16BIT; + } catch(UnsupportedEncodingException uex) { + Rlog.e(LOG_TAG, + "Implausible UnsupportedEncodingException ", + uex); + return null; + } + } + + if (encoding == ENCODING_7BIT) { + if ((0xff & userData[0]) > MAX_USER_DATA_SEPTETS) { + // Message too long + Rlog.e(LOG_TAG, "Message too long (" + (0xff & userData[0]) + " septets)"); + return null; + } + // TP-Data-Coding-Scheme + // Default encoding, uncompressed + // To test writing messages to the SIM card, change this value 0x00 + // to 0x12, which means "bits 1 and 0 contain message class, and the + // class is 2". Note that this takes effect for the sender. In other + // words, messages sent by the phone with this change will end up on + // the receiver's SIM card. You can then send messages to yourself + // (on a phone with this change) and they'll end up on the SIM card. + bo.write(0x00); + } else { // assume UCS-2 + if ((0xff & userData[0]) > MAX_USER_DATA_BYTES) { + // Message too long + Rlog.e(LOG_TAG, "Message too long (" + (0xff & userData[0]) + " bytes)"); + return null; + } + // TP-Data-Coding-Scheme + // UCS-2 encoding, uncompressed + bo.write(0x08); + } + + // (no TP-Validity-Period) + bo.write(userData, 0, userData.length); + ret.encodedMessage = bo.toByteArray(); + return ret; + } + + /** + * Packs header and UCS-2 encoded message. Includes TP-UDL & TP-UDHL if necessary + * + * @return encoded message as UCS2 + * @throws UnsupportedEncodingException + */ + private static byte[] encodeUCS2(String message, byte[] header) + throws UnsupportedEncodingException { + byte[] userData, textPart; + textPart = message.getBytes("utf-16be"); + + if (header != null) { + // Need 1 byte for UDHL + userData = new byte[header.length + textPart.length + 1]; + + userData[0] = (byte)header.length; + System.arraycopy(header, 0, userData, 1, header.length); + System.arraycopy(textPart, 0, userData, header.length + 1, textPart.length); + } + else { + userData = textPart; + } + byte[] ret = new byte[userData.length+1]; + ret[0] = (byte) (userData.length & 0xff ); + System.arraycopy(userData, 0, ret, 1, userData.length); + return ret; + } + + /** + * Get an SMS-SUBMIT PDU for a destination address and a message + * + * @param scAddress Service Centre address. Null means use default. + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + */ + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, String message, + boolean statusReportRequested) { + + return getSubmitPdu(scAddress, destinationAddress, message, statusReportRequested, null); + } + + /** + * Get an SMS-SUBMIT PDU for a data message to a destination address & port + * + * @param scAddress Service Centre address. null == use default + * @param destinationAddress the address of the destination for the message + * @param destinationPort the port to deliver the message to at the + * destination + * @param data the data for the message + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + */ + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, int destinationPort, byte[] data, + boolean statusReportRequested) { + + SmsHeader.PortAddrs portAddrs = new SmsHeader.PortAddrs(); + portAddrs.destPort = destinationPort; + portAddrs.origPort = 0; + portAddrs.areEightBits = false; + + SmsHeader smsHeader = new SmsHeader(); + smsHeader.portAddrs = portAddrs; + + byte[] smsHeaderData = SmsHeader.toByteArray(smsHeader); + + if ((data.length + smsHeaderData.length + 1) > MAX_USER_DATA_BYTES) { + Rlog.e(LOG_TAG, "SMS data message may only contain " + + (MAX_USER_DATA_BYTES - smsHeaderData.length - 1) + " bytes"); + return null; + } + + SubmitPdu ret = new SubmitPdu(); + ByteArrayOutputStream bo = getSubmitPduHead( + scAddress, destinationAddress, (byte) 0x41, // MTI = SMS-SUBMIT, + // TP-UDHI = true + statusReportRequested, ret); + + // TP-Data-Coding-Scheme + // No class, 8 bit data + bo.write(0x04); + + // (no TP-Validity-Period) + + // Total size + bo.write(data.length + smsHeaderData.length + 1); + + // User data header + bo.write(smsHeaderData.length); + bo.write(smsHeaderData, 0, smsHeaderData.length); + + // User data + bo.write(data, 0, data.length); + + ret.encodedMessage = bo.toByteArray(); + return ret; + } + + /** + * Create the beginning of a SUBMIT PDU. This is the part of the + * SUBMIT PDU that is common to the two versions of {@link #getSubmitPdu}, + * one of which takes a byte array and the other of which takes a + * <code>String</code>. + * + * @param scAddress Service Centre address. null == use default + * @param destinationAddress the address of the destination for the message + * @param mtiByte + * @param ret <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message + */ + private static ByteArrayOutputStream getSubmitPduHead( + String scAddress, String destinationAddress, byte mtiByte, + boolean statusReportRequested, SubmitPdu ret) { + ByteArrayOutputStream bo = new ByteArrayOutputStream( + MAX_USER_DATA_BYTES + 40); + + // SMSC address with length octet, or 0 + if (scAddress == null) { + ret.encodedScAddress = null; + } else { + ret.encodedScAddress = PhoneNumberUtils.networkPortionToCalledPartyBCDWithLength( + scAddress); + } + + // TP-Message-Type-Indicator (and friends) + if (statusReportRequested) { + // Set TP-Status-Report-Request bit. + mtiByte |= 0x20; + if (VDBG) Rlog.d(LOG_TAG, "SMS status report requested"); + } + bo.write(mtiByte); + + // space for TP-Message-Reference + bo.write(0); + + byte[] daBytes; + + daBytes = PhoneNumberUtils.networkPortionToCalledPartyBCD(destinationAddress); + + // destination address length in BCD digits, ignoring TON byte and pad + // TODO Should be better. + bo.write((daBytes.length - 1) * 2 + - ((daBytes[daBytes.length - 1] & 0xf0) == 0xf0 ? 1 : 0)); + + // destination address + bo.write(daBytes, 0, daBytes.length); + + // TP-Protocol-Identifier + bo.write(0); + return bo; + } + + private static class PduParser { + byte mPdu[]; + int mCur; + SmsHeader mUserDataHeader; + byte[] mUserData; + int mUserDataSeptetPadding; + + PduParser(byte[] pdu) { + mPdu = pdu; + mCur = 0; + mUserDataSeptetPadding = 0; + } + + /** + * Parse and return the SC address prepended to SMS messages coming via + * the TS 27.005 / AT interface. Returns null on invalid address + */ + String getSCAddress() { + int len; + String ret; + + // length of SC Address + len = getByte(); + + if (len == 0) { + // no SC address + ret = null; + } else { + // SC address + try { + ret = PhoneNumberUtils + .calledPartyBCDToString(mPdu, mCur, len); + } catch (RuntimeException tr) { + Rlog.d(LOG_TAG, "invalid SC address: ", tr); + ret = null; + } + } + + mCur += len; + + return ret; + } + + /** + * returns non-sign-extended byte value + */ + int getByte() { + return mPdu[mCur++] & 0xff; + } + + /** + * Any address except the SC address (eg, originating address) See TS + * 23.040 9.1.2.5 + */ + GsmSmsAddress getAddress() { + GsmSmsAddress ret; + + // "The Address-Length field is an integer representation of + // the number field, i.e. excludes any semi-octet containing only + // fill bits." + // The TOA field is not included as part of this + int addressLength = mPdu[mCur] & 0xff; + int lengthBytes = 2 + (addressLength + 1) / 2; + + try { + ret = new GsmSmsAddress(mPdu, mCur, lengthBytes); + } catch (ParseException e) { + ret = null; + //This is caught by createFromPdu(byte[] pdu) + throw new RuntimeException(e.getMessage()); + } + + mCur += lengthBytes; + + return ret; + } + + /** + * Parses an SC timestamp and returns a currentTimeMillis()-style + * timestamp + */ + + long getSCTimestampMillis() { + // TP-Service-Centre-Time-Stamp + int year = IccUtils.gsmBcdByteToInt(mPdu[mCur++]); + int month = IccUtils.gsmBcdByteToInt(mPdu[mCur++]); + int day = IccUtils.gsmBcdByteToInt(mPdu[mCur++]); + int hour = IccUtils.gsmBcdByteToInt(mPdu[mCur++]); + int minute = IccUtils.gsmBcdByteToInt(mPdu[mCur++]); + int second = IccUtils.gsmBcdByteToInt(mPdu[mCur++]); + + // For the timezone, the most significant bit of the + // least significant nibble is the sign byte + // (meaning the max range of this field is 79 quarter-hours, + // which is more than enough) + + byte tzByte = mPdu[mCur++]; + + // Mask out sign bit. + int timezoneOffset = IccUtils.gsmBcdByteToInt((byte) (tzByte & (~0x08))); + + timezoneOffset = ((tzByte & 0x08) == 0) ? timezoneOffset : -timezoneOffset; + + Time time = new Time(Time.TIMEZONE_UTC); + + // It's 2006. Should I really support years < 2000? + time.year = year >= 90 ? year + 1900 : year + 2000; + time.month = month - 1; + time.monthDay = day; + time.hour = hour; + time.minute = minute; + time.second = second; + + // Timezone offset is in quarter hours. + return time.toMillis(true) - (timezoneOffset * 15 * 60 * 1000); + } + + /** + * Pulls the user data out of the PDU, and separates the payload from + * the header if there is one. + * + * @param hasUserDataHeader true if there is a user data header + * @param dataInSeptets true if the data payload is in septets instead + * of octets + * @return the number of septets or octets in the user data payload + */ + int constructUserData(boolean hasUserDataHeader, boolean dataInSeptets) { + int offset = mCur; + int userDataLength = mPdu[offset++] & 0xff; + int headerSeptets = 0; + int userDataHeaderLength = 0; + + if (hasUserDataHeader) { + userDataHeaderLength = mPdu[offset++] & 0xff; + + byte[] udh = new byte[userDataHeaderLength]; + System.arraycopy(mPdu, offset, udh, 0, userDataHeaderLength); + mUserDataHeader = SmsHeader.fromByteArray(udh); + offset += userDataHeaderLength; + + int headerBits = (userDataHeaderLength + 1) * 8; + headerSeptets = headerBits / 7; + headerSeptets += (headerBits % 7) > 0 ? 1 : 0; + mUserDataSeptetPadding = (headerSeptets * 7) - headerBits; + } + + int bufferLen; + if (dataInSeptets) { + /* + * Here we just create the user data length to be the remainder of + * the pdu minus the user data header, since userDataLength means + * the number of uncompressed septets. + */ + bufferLen = mPdu.length - offset; + } else { + /* + * userDataLength is the count of octets, so just subtract the + * user data header. + */ + bufferLen = userDataLength - (hasUserDataHeader ? (userDataHeaderLength + 1) : 0); + if (bufferLen < 0) { + bufferLen = 0; + } + } + + mUserData = new byte[bufferLen]; + System.arraycopy(mPdu, offset, mUserData, 0, mUserData.length); + mCur = offset; + + if (dataInSeptets) { + // Return the number of septets + int count = userDataLength - headerSeptets; + // If count < 0, return 0 (means UDL was probably incorrect) + return count < 0 ? 0 : count; + } else { + // Return the number of octets + return mUserData.length; + } + } + + /** + * Returns the user data payload, not including the headers + * + * @return the user data payload, not including the headers + */ + byte[] getUserData() { + return mUserData; + } + + /** + * Returns an object representing the user data headers + * + * {@hide} + */ + SmsHeader getUserDataHeader() { + return mUserDataHeader; + } + + /** + * Interprets the user data payload as packed GSM 7bit characters, and + * decodes them into a String. + * + * @param septetCount the number of septets in the user data payload + * @return a String with the decoded characters + */ + String getUserDataGSM7Bit(int septetCount, int languageTable, + int languageShiftTable) { + String ret; + + ret = GsmAlphabet.gsm7BitPackedToString(mPdu, mCur, septetCount, + mUserDataSeptetPadding, languageTable, languageShiftTable); + + mCur += (septetCount * 7) / 8; + + return ret; + } + + /** + * Interprets the user data payload as pack GSM 8-bit (a GSM alphabet string that's + * stored in 8-bit unpacked format) characters, and decodes them into a String. + * + * @param byteCount the number of byest in the user data payload + * @return a String with the decoded characters + */ + String getUserDataGSM8bit(int byteCount) { + String ret; + + ret = GsmAlphabet.gsm8BitUnpackedToString(mPdu, mCur, byteCount); + + mCur += byteCount; + + return ret; + } + + /** + * Interprets the user data payload as UCS2 characters, and + * decodes them into a String. + * + * @param byteCount the number of bytes in the user data payload + * @return a String with the decoded characters + */ + String getUserDataUCS2(int byteCount) { + String ret; + + try { + ret = new String(mPdu, mCur, byteCount, "utf-16"); + } catch (UnsupportedEncodingException ex) { + ret = ""; + Rlog.e(LOG_TAG, "implausible UnsupportedEncodingException", ex); + } + + mCur += byteCount; + return ret; + } + + /** + * Interprets the user data payload as KSC-5601 characters, and + * decodes them into a String. + * + * @param byteCount the number of bytes in the user data payload + * @return a String with the decoded characters + */ + String getUserDataKSC5601(int byteCount) { + String ret; + + try { + ret = new String(mPdu, mCur, byteCount, "KSC5601"); + } catch (UnsupportedEncodingException ex) { + ret = ""; + Rlog.e(LOG_TAG, "implausible UnsupportedEncodingException", ex); + } + + mCur += byteCount; + return ret; + } + + boolean moreDataPresent() { + return (mPdu.length > mCur); + } + } + + /** + * Calculates the number of SMS's required to encode the message body and + * the number of characters remaining until the next message. + * + * @param msgBody the message to encode + * @param use7bitOnly ignore (but still count) illegal characters if true + * @return TextEncodingDetails + */ + public static TextEncodingDetails calculateLength(CharSequence msgBody, + boolean use7bitOnly) { + CharSequence newMsgBody = null; + Resources r = Resources.getSystem(); + if (r.getBoolean(com.android.internal.R.bool.config_sms_force_7bit_encoding)) { + newMsgBody = Sms7BitEncodingTranslator.translate(msgBody); + } + if (TextUtils.isEmpty(newMsgBody)) { + newMsgBody = msgBody; + } + TextEncodingDetails ted = GsmAlphabet.countGsmSeptets(newMsgBody, use7bitOnly); + if (ted == null) { + return SmsMessageBase.calcUnicodeEncodingDetails(newMsgBody); + } + return ted; + } + + /** {@inheritDoc} */ + @Override + public int getProtocolIdentifier() { + return mProtocolIdentifier; + } + + /** + * Returns the TP-Data-Coding-Scheme byte, for acknowledgement of SMS-PP download messages. + * @return the TP-DCS field of the SMS header + */ + int getDataCodingScheme() { + return mDataCodingScheme; + } + + /** {@inheritDoc} */ + @Override + public boolean isReplace() { + return (mProtocolIdentifier & 0xc0) == 0x40 + && (mProtocolIdentifier & 0x3f) > 0 + && (mProtocolIdentifier & 0x3f) < 8; + } + + /** {@inheritDoc} */ + @Override + public boolean isCphsMwiMessage() { + return ((GsmSmsAddress) mOriginatingAddress).isCphsVoiceMessageClear() + || ((GsmSmsAddress) mOriginatingAddress).isCphsVoiceMessageSet(); + } + + /** {@inheritDoc} */ + @Override + public boolean isMWIClearMessage() { + if (mIsMwi && !mMwiSense) { + return true; + } + + return mOriginatingAddress != null + && ((GsmSmsAddress) mOriginatingAddress).isCphsVoiceMessageClear(); + } + + /** {@inheritDoc} */ + @Override + public boolean isMWISetMessage() { + if (mIsMwi && mMwiSense) { + return true; + } + + return mOriginatingAddress != null + && ((GsmSmsAddress) mOriginatingAddress).isCphsVoiceMessageSet(); + } + + /** {@inheritDoc} */ + @Override + public boolean isMwiDontStore() { + if (mIsMwi && mMwiDontStore) { + return true; + } + + if (isCphsMwiMessage()) { + // See CPHS 4.2 Section B.4.2.1 + // If the user data is a single space char, do not store + // the message. Otherwise, store and display as usual + if (" ".equals(getMessageBody())) { + return true; + } + } + + return false; + } + + /** {@inheritDoc} */ + @Override + public int getStatus() { + return mStatus; + } + + /** {@inheritDoc} */ + @Override + public boolean isStatusReportMessage() { + return mIsStatusReportMessage; + } + + /** {@inheritDoc} */ + @Override + public boolean isReplyPathPresent() { + return mReplyPathPresent; + } + + /** + * TS 27.005 3.1, <pdu> definition "In the case of SMS: 3GPP TS 24.011 [6] + * SC address followed by 3GPP TS 23.040 [3] TPDU in hexadecimal format: + * ME/TA converts each octet of TP data unit into two IRA character long + * hex number (e.g. octet with integer value 42 is presented to TE as two + * characters 2A (IRA 50 and 65))" ...in the case of cell broadcast, + * something else... + */ + private void parsePdu(byte[] pdu) { + mPdu = pdu; + // Rlog.d(LOG_TAG, "raw sms message:"); + // Rlog.d(LOG_TAG, s); + + PduParser p = new PduParser(pdu); + + mScAddress = p.getSCAddress(); + + if (mScAddress != null) { + if (VDBG) Rlog.d(LOG_TAG, "SMS SC address: " + mScAddress); + } + + // TODO(mkf) support reply path, user data header indicator + + // TP-Message-Type-Indicator + // 9.2.3 + int firstByte = p.getByte(); + + mMti = firstByte & 0x3; + switch (mMti) { + // TP-Message-Type-Indicator + // 9.2.3 + case 0: + case 3: //GSM 03.40 9.2.3.1: MTI == 3 is Reserved. + //This should be processed in the same way as MTI == 0 (Deliver) + parseSmsDeliver(p, firstByte); + break; + case 1: + parseSmsSubmit(p, firstByte); + break; + case 2: + parseSmsStatusReport(p, firstByte); + break; + default: + // TODO(mkf) the rest of these + throw new RuntimeException("Unsupported message type"); + } + } + + /** + * Parses a SMS-STATUS-REPORT message. + * + * @param p A PduParser, cued past the first byte. + * @param firstByte The first byte of the PDU, which contains MTI, etc. + */ + private void parseSmsStatusReport(PduParser p, int firstByte) { + mIsStatusReportMessage = true; + + // TP-Message-Reference + mMessageRef = p.getByte(); + // TP-Recipient-Address + mRecipientAddress = p.getAddress(); + // TP-Service-Centre-Time-Stamp + mScTimeMillis = p.getSCTimestampMillis(); + p.getSCTimestampMillis(); + // TP-Status + mStatus = p.getByte(); + + // The following are optional fields that may or may not be present. + if (p.moreDataPresent()) { + // TP-Parameter-Indicator + int extraParams = p.getByte(); + int moreExtraParams = extraParams; + while ((moreExtraParams & 0x80) != 0) { + // We only know how to parse a few extra parameters, all + // indicated in the first TP-PI octet, so skip over any + // additional TP-PI octets. + moreExtraParams = p.getByte(); + } + // As per 3GPP 23.040 section 9.2.3.27 TP-Parameter-Indicator, + // only process the byte if the reserved bits (bits3 to 6) are zero. + if ((extraParams & 0x78) == 0) { + // TP-Protocol-Identifier + if ((extraParams & 0x01) != 0) { + mProtocolIdentifier = p.getByte(); + } + // TP-Data-Coding-Scheme + if ((extraParams & 0x02) != 0) { + mDataCodingScheme = p.getByte(); + } + // TP-User-Data-Length (implies existence of TP-User-Data) + if ((extraParams & 0x04) != 0) { + boolean hasUserDataHeader = (firstByte & 0x40) == 0x40; + parseUserData(p, hasUserDataHeader); + } + } + } + } + + private void parseSmsDeliver(PduParser p, int firstByte) { + mReplyPathPresent = (firstByte & 0x80) == 0x80; + + mOriginatingAddress = p.getAddress(); + + if (mOriginatingAddress != null) { + if (VDBG) Rlog.v(LOG_TAG, "SMS originating address: " + + mOriginatingAddress.address); + } + + // TP-Protocol-Identifier (TP-PID) + // TS 23.040 9.2.3.9 + mProtocolIdentifier = p.getByte(); + + // TP-Data-Coding-Scheme + // see TS 23.038 + mDataCodingScheme = p.getByte(); + + if (VDBG) { + Rlog.v(LOG_TAG, "SMS TP-PID:" + mProtocolIdentifier + + " data coding scheme: " + mDataCodingScheme); + } + + mScTimeMillis = p.getSCTimestampMillis(); + + if (VDBG) Rlog.d(LOG_TAG, "SMS SC timestamp: " + mScTimeMillis); + + boolean hasUserDataHeader = (firstByte & 0x40) == 0x40; + + parseUserData(p, hasUserDataHeader); + } + + /** + * Parses a SMS-SUBMIT message. + * + * @param p A PduParser, cued past the first byte. + * @param firstByte The first byte of the PDU, which contains MTI, etc. + */ + private void parseSmsSubmit(PduParser p, int firstByte) { + mReplyPathPresent = (firstByte & 0x80) == 0x80; + + // TP-MR (TP-Message Reference) + mMessageRef = p.getByte(); + + mRecipientAddress = p.getAddress(); + + if (mRecipientAddress != null) { + if (VDBG) Rlog.v(LOG_TAG, "SMS recipient address: " + mRecipientAddress.address); + } + + // TP-Protocol-Identifier (TP-PID) + // TS 23.040 9.2.3.9 + mProtocolIdentifier = p.getByte(); + + // TP-Data-Coding-Scheme + // see TS 23.038 + mDataCodingScheme = p.getByte(); + + if (VDBG) { + Rlog.v(LOG_TAG, "SMS TP-PID:" + mProtocolIdentifier + + " data coding scheme: " + mDataCodingScheme); + } + + // TP-Validity-Period-Format + int validityPeriodLength = 0; + int validityPeriodFormat = ((firstByte>>3) & 0x3); + if (0x0 == validityPeriodFormat) /* 00, TP-VP field not present*/ + { + validityPeriodLength = 0; + } + else if (0x2 == validityPeriodFormat) /* 10, TP-VP: relative format*/ + { + validityPeriodLength = 1; + } + else /* other case, 11 or 01, TP-VP: absolute or enhanced format*/ + { + validityPeriodLength = 7; + } + + // TP-Validity-Period is not used on phone, so just ignore it for now. + while (validityPeriodLength-- > 0) + { + p.getByte(); + } + + boolean hasUserDataHeader = (firstByte & 0x40) == 0x40; + + parseUserData(p, hasUserDataHeader); + } + + /** + * Parses the User Data of an SMS. + * + * @param p The current PduParser. + * @param hasUserDataHeader Indicates whether a header is present in the + * User Data. + */ + private void parseUserData(PduParser p, boolean hasUserDataHeader) { + boolean hasMessageClass = false; + boolean userDataCompressed = false; + + int encodingType = ENCODING_UNKNOWN; + + // Look up the data encoding scheme + if ((mDataCodingScheme & 0x80) == 0) { + userDataCompressed = (0 != (mDataCodingScheme & 0x20)); + hasMessageClass = (0 != (mDataCodingScheme & 0x10)); + + if (userDataCompressed) { + Rlog.w(LOG_TAG, "4 - Unsupported SMS data coding scheme " + + "(compression) " + (mDataCodingScheme & 0xff)); + } else { + switch ((mDataCodingScheme >> 2) & 0x3) { + case 0: // GSM 7 bit default alphabet + encodingType = ENCODING_7BIT; + break; + + case 2: // UCS 2 (16bit) + encodingType = ENCODING_16BIT; + break; + + case 1: // 8 bit data + //Support decoding the user data payload as pack GSM 8-bit (a GSM alphabet string + //that's stored in 8-bit unpacked format) characters. + Resources r = Resources.getSystem(); + if (r.getBoolean(com.android.internal. + R.bool.config_sms_decode_gsm_8bit_data)) { + encodingType = ENCODING_8BIT; + break; + } + + case 3: // reserved + Rlog.w(LOG_TAG, "1 - Unsupported SMS data coding scheme " + + (mDataCodingScheme & 0xff)); + encodingType = ENCODING_8BIT; + break; + } + } + } else if ((mDataCodingScheme & 0xf0) == 0xf0) { + hasMessageClass = true; + userDataCompressed = false; + + if (0 == (mDataCodingScheme & 0x04)) { + // GSM 7 bit default alphabet + encodingType = ENCODING_7BIT; + } else { + // 8 bit data + encodingType = ENCODING_8BIT; + } + } else if ((mDataCodingScheme & 0xF0) == 0xC0 + || (mDataCodingScheme & 0xF0) == 0xD0 + || (mDataCodingScheme & 0xF0) == 0xE0) { + // 3GPP TS 23.038 V7.0.0 (2006-03) section 4 + + // 0xC0 == 7 bit, don't store + // 0xD0 == 7 bit, store + // 0xE0 == UCS-2, store + + if ((mDataCodingScheme & 0xF0) == 0xE0) { + encodingType = ENCODING_16BIT; + } else { + encodingType = ENCODING_7BIT; + } + + userDataCompressed = false; + boolean active = ((mDataCodingScheme & 0x08) == 0x08); + // bit 0x04 reserved + + // VM - If TP-UDH is present, these values will be overwritten + if ((mDataCodingScheme & 0x03) == 0x00) { + mIsMwi = true; /* Indicates vmail */ + mMwiSense = active;/* Indicates vmail notification set/clear */ + mMwiDontStore = ((mDataCodingScheme & 0xF0) == 0xC0); + + /* Set voice mail count based on notification bit */ + if (active == true) { + mVoiceMailCount = -1; // unknown number of messages waiting + } else { + mVoiceMailCount = 0; // no unread messages + } + + Rlog.w(LOG_TAG, "MWI in DCS for Vmail. DCS = " + + (mDataCodingScheme & 0xff) + " Dont store = " + + mMwiDontStore + " vmail count = " + mVoiceMailCount); + + } else { + mIsMwi = false; + Rlog.w(LOG_TAG, "MWI in DCS for fax/email/other: " + + (mDataCodingScheme & 0xff)); + } + } else if ((mDataCodingScheme & 0xC0) == 0x80) { + // 3GPP TS 23.038 V7.0.0 (2006-03) section 4 + // 0x80..0xBF == Reserved coding groups + if (mDataCodingScheme == 0x84) { + // This value used for KSC5601 by carriers in Korea. + encodingType = ENCODING_KSC5601; + } else { + Rlog.w(LOG_TAG, "5 - Unsupported SMS data coding scheme " + + (mDataCodingScheme & 0xff)); + } + } else { + Rlog.w(LOG_TAG, "3 - Unsupported SMS data coding scheme " + + (mDataCodingScheme & 0xff)); + } + + // set both the user data and the user data header. + int count = p.constructUserData(hasUserDataHeader, + encodingType == ENCODING_7BIT); + this.mUserData = p.getUserData(); + this.mUserDataHeader = p.getUserDataHeader(); + + /* + * Look for voice mail indication in TP_UDH TS23.040 9.2.3.24 + * ieid = 1 (0x1) (SPECIAL_SMS_MSG_IND) + * ieidl =2 octets + * ieda msg_ind_type = 0x00 (voice mail; discard sms )or + * = 0x80 (voice mail; store sms) + * msg_count = 0x00 ..0xFF + */ + if (hasUserDataHeader && (mUserDataHeader.specialSmsMsgList.size() != 0)) { + for (SmsHeader.SpecialSmsMsg msg : mUserDataHeader.specialSmsMsgList) { + int msgInd = msg.msgIndType & 0xff; + /* + * TS 23.040 V6.8.1 Sec 9.2.3.24.2 + * bits 1 0 : basic message indication type + * bits 4 3 2 : extended message indication type + * bits 6 5 : Profile id bit 7 storage type + */ + if ((msgInd == 0) || (msgInd == 0x80)) { + mIsMwi = true; + if (msgInd == 0x80) { + /* Store message because TP_UDH indicates so*/ + mMwiDontStore = false; + } else if (mMwiDontStore == false) { + /* Storage bit is not set by TP_UDH + * Check for conflict + * between message storage bit in TP_UDH + * & DCS. The message shall be stored if either of + * the one indicates so. + * TS 23.040 V6.8.1 Sec 9.2.3.24.2 + */ + if (!((((mDataCodingScheme & 0xF0) == 0xD0) + || ((mDataCodingScheme & 0xF0) == 0xE0)) + && ((mDataCodingScheme & 0x03) == 0x00))) { + /* Even DCS did not have voice mail with Storage bit + * 3GPP TS 23.038 V7.0.0 section 4 + * So clear this flag*/ + mMwiDontStore = true; + } + } + + mVoiceMailCount = msg.msgCount & 0xff; + + /* + * In the event of a conflict between message count setting + * and DCS then the Message Count in the TP-UDH shall + * override the indication in the TP-DCS. Set voice mail + * notification based on count in TP-UDH + */ + if (mVoiceMailCount > 0) + mMwiSense = true; + else + mMwiSense = false; + + Rlog.w(LOG_TAG, "MWI in TP-UDH for Vmail. Msg Ind = " + msgInd + + " Dont store = " + mMwiDontStore + " Vmail count = " + + mVoiceMailCount); + + /* + * There can be only one IE for each type of message + * indication in TP_UDH. In the event they are duplicated + * last occurence will be used. Hence the for loop + */ + } else { + Rlog.w(LOG_TAG, "TP_UDH fax/email/" + + "extended msg/multisubscriber profile. Msg Ind = " + msgInd); + } + } // end of for + } // end of if UDH + + switch (encodingType) { + case ENCODING_UNKNOWN: + mMessageBody = null; + break; + + case ENCODING_8BIT: + //Support decoding the user data payload as pack GSM 8-bit (a GSM alphabet string + //that's stored in 8-bit unpacked format) characters. + Resources r = Resources.getSystem(); + if (r.getBoolean(com.android.internal. + R.bool.config_sms_decode_gsm_8bit_data)) { + mMessageBody = p.getUserDataGSM8bit(count); + } else { + mMessageBody = null; + } + break; + + case ENCODING_7BIT: + mMessageBody = p.getUserDataGSM7Bit(count, + hasUserDataHeader ? mUserDataHeader.languageTable : 0, + hasUserDataHeader ? mUserDataHeader.languageShiftTable : 0); + break; + + case ENCODING_16BIT: + mMessageBody = p.getUserDataUCS2(count); + break; + + case ENCODING_KSC5601: + mMessageBody = p.getUserDataKSC5601(count); + break; + } + + if (VDBG) Rlog.v(LOG_TAG, "SMS message body (raw): '" + mMessageBody + "'"); + + if (mMessageBody != null) { + parseMessageBody(); + } + + if (!hasMessageClass) { + messageClass = MessageClass.UNKNOWN; + } else { + switch (mDataCodingScheme & 0x3) { + case 0: + messageClass = MessageClass.CLASS_0; + break; + case 1: + messageClass = MessageClass.CLASS_1; + break; + case 2: + messageClass = MessageClass.CLASS_2; + break; + case 3: + messageClass = MessageClass.CLASS_3; + break; + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public MessageClass getMessageClass() { + return messageClass; + } + + /** + * Returns true if this is a (U)SIM data download type SM. + * See 3GPP TS 31.111 section 9.1 and TS 23.040 section 9.2.3.9. + * + * @return true if this is a USIM data download message; false otherwise + */ + boolean isUsimDataDownload() { + return messageClass == MessageClass.CLASS_2 && + (mProtocolIdentifier == 0x7f || mProtocolIdentifier == 0x7c); + } + + public int getNumOfVoicemails() { + /* + * Order of priority if multiple indications are present is 1.UDH, + * 2.DCS, 3.CPHS. + * Voice mail count if voice mail present indication is + * received + * 1. UDH (or both UDH & DCS): mVoiceMailCount = 0 to 0xff. Ref[TS 23. 040] + * 2. DCS only: count is unknown mVoiceMailCount= -1 + * 3. CPHS only: count is unknown mVoiceMailCount = 0xff. Ref[GSM-BTR-1-4700] + * Voice mail clear, mVoiceMailCount = 0. + */ + if ((!mIsMwi) && isCphsMwiMessage()) { + if (mOriginatingAddress != null + && ((GsmSmsAddress) mOriginatingAddress).isCphsVoiceMessageSet()) { + mVoiceMailCount = 0xff; + } else { + mVoiceMailCount = 0; + } + Rlog.v(LOG_TAG, "CPHS voice mail message"); + } + return mVoiceMailCount; + } +} |