/* * 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 android.app.Activity; import android.app.AlertDialog; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.Cursor; import android.database.SQLException; import android.net.Uri; import android.os.AsyncResult; import android.os.Binder; import android.os.Handler; import android.os.Message; import android.os.PowerManager; import android.os.SystemProperties; import android.provider.Telephony; import android.provider.Telephony.Sms.Intents; import android.telephony.PhoneNumberUtils; import android.telephony.ServiceState; import android.telephony.SmsCbMessage; import android.telephony.SmsMessage; import android.telephony.TelephonyManager; import android.text.Html; import android.text.Spanned; import android.util.Log; import android.view.WindowManager; import com.android.internal.R; import com.android.internal.telephony.SmsMessageBase.TextEncodingDetails; import com.android.internal.util.HexDump; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Random; import static android.telephony.SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE; import static android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE; import static android.telephony.SmsManager.RESULT_ERROR_LIMIT_EXCEEDED; import static android.telephony.SmsManager.RESULT_ERROR_NO_SERVICE; import static android.telephony.SmsManager.RESULT_ERROR_NULL_PDU; import static android.telephony.SmsManager.RESULT_ERROR_RADIO_OFF; public abstract class SMSDispatcher extends Handler { static final String TAG = "SMS"; // accessed from inner class private static final String SEND_NEXT_MSG_EXTRA = "SendNextMsg"; /** Permission required to receive SMS and SMS-CB messages. */ public static final String RECEIVE_SMS_PERMISSION = "android.permission.RECEIVE_SMS"; /** Permission required to receive ETWS and CMAS emergency broadcasts. */ public static final String RECEIVE_EMERGENCY_BROADCAST_PERMISSION = "android.permission.RECEIVE_EMERGENCY_BROADCAST"; /** Permission required to send SMS to short codes without user confirmation. */ private static final String SEND_SMS_NO_CONFIRMATION_PERMISSION = "android.permission.SEND_SMS_NO_CONFIRMATION"; /** Query projection for checking for duplicate message segments. */ private static final String[] PDU_PROJECTION = new String[] { "pdu" }; /** Query projection for combining concatenated message segments. */ private static final String[] PDU_SEQUENCE_PORT_PROJECTION = new String[] { "pdu", "sequence", "destination_port" }; private static final int PDU_COLUMN = 0; private static final int SEQUENCE_COLUMN = 1; private static final int DESTINATION_PORT_COLUMN = 2; /** New SMS received. */ protected static final int EVENT_NEW_SMS = 1; /** SMS send complete. */ protected static final int EVENT_SEND_SMS_COMPLETE = 2; /** Retry sending a previously failed SMS message */ private static final int EVENT_SEND_RETRY = 3; /** Confirmation required for sending a large number of messages. */ private static final int EVENT_SEND_LIMIT_REACHED_CONFIRMATION = 4; /** Send the user confirmed SMS */ static final int EVENT_SEND_CONFIRMED_SMS = 5; // accessed from inner class /** Don't send SMS (user did not confirm). */ static final int EVENT_STOP_SENDING = 7; // accessed from inner class protected final Phone mPhone; protected final Context mContext; protected final ContentResolver mResolver; protected final CommandsInterface mCm; protected final SmsStorageMonitor mStorageMonitor; protected final TelephonyManager mTelephonyManager; protected final WapPushOverSms mWapPush; protected static final Uri mRawUri = Uri.withAppendedPath(Telephony.Sms.CONTENT_URI, "raw"); /** Maximum number of times to retry sending a failed SMS. */ private static final int MAX_SEND_RETRIES = 3; /** Delay before next send attempt on a failed SMS, in milliseconds. */ private static final int SEND_RETRY_DELAY = 2000; /** single part SMS */ private static final int SINGLE_PART_SMS = 1; /** Message sending queue limit */ private static final int MO_MSG_QUEUE_LIMIT = 5; /** * Message reference for a CONCATENATED_8_BIT_REFERENCE or * CONCATENATED_16_BIT_REFERENCE message set. Should be * incremented for each set of concatenated messages. * Static field shared by all dispatcher objects. */ private static int sConcatenatedRef = new Random().nextInt(256); /** Outgoing message counter. Shared by all dispatchers. */ private final SmsUsageMonitor mUsageMonitor; /** Number of outgoing SmsTrackers waiting for user confirmation. */ private int mPendingTrackerCount; /** Wake lock to ensure device stays awake while dispatching the SMS intent. */ private PowerManager.WakeLock mWakeLock; /** * Hold the wake lock for 5 seconds, which should be enough time for * any receiver(s) to grab its own wake lock. */ private static final int WAKE_LOCK_TIMEOUT = 5000; /* Flags indicating whether the current device allows sms service */ protected boolean mSmsCapable = true; protected boolean mSmsReceiveDisabled; protected boolean mSmsSendDisabled; protected int mRemainingMessages = -1; protected static int getNextConcatenatedRef() { sConcatenatedRef += 1; return sConcatenatedRef; } /** * Create a new SMS dispatcher. * @param phone the Phone to use * @param storageMonitor the SmsStorageMonitor to use * @param usageMonitor the SmsUsageMonitor to use */ protected SMSDispatcher(PhoneBase phone, SmsStorageMonitor storageMonitor, SmsUsageMonitor usageMonitor) { mPhone = phone; mWapPush = new WapPushOverSms(phone, this); mContext = phone.getContext(); mResolver = mContext.getContentResolver(); mCm = phone.mCM; mStorageMonitor = storageMonitor; mUsageMonitor = usageMonitor; mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); createWakelock(); mSmsCapable = mContext.getResources().getBoolean( com.android.internal.R.bool.config_sms_capable); mSmsReceiveDisabled = !SystemProperties.getBoolean( TelephonyProperties.PROPERTY_SMS_RECEIVE, mSmsCapable); mSmsSendDisabled = !SystemProperties.getBoolean( TelephonyProperties.PROPERTY_SMS_SEND, mSmsCapable); Log.d(TAG, "SMSDispatcher: ctor mSmsCapable=" + mSmsCapable + " format=" + getFormat() + " mSmsReceiveDisabled=" + mSmsReceiveDisabled + " mSmsSendDisabled=" + mSmsSendDisabled); } /** Unregister for incoming SMS events. */ public abstract void dispose(); /** * The format of the message PDU in the associated broadcast intent. * This will be either "3gpp" for GSM/UMTS/LTE messages in 3GPP format * or "3gpp2" for CDMA/LTE messages in 3GPP2 format. * * Note: All applications which handle incoming SMS messages by processing the * SMS_RECEIVED_ACTION broadcast intent MUST pass the "format" extra from the intent * into the new methods in {@link android.telephony.SmsMessage} which take an * extra format parameter. This is required in order to correctly decode the PDU on * devices which require support for both 3GPP and 3GPP2 formats at the same time, * such as CDMA/LTE devices and GSM/CDMA world phones. * * @return the format of the message PDU */ protected abstract String getFormat(); @Override protected void finalize() { Log.d(TAG, "SMSDispatcher finalized"); } /* TODO: Need to figure out how to keep track of status report routing in a * persistent manner. If the phone process restarts (reboot or crash), * we will lose this list and any status reports that come in after * will be dropped. */ /** Sent messages awaiting a delivery status report. */ protected final ArrayList deliveryPendingList = new ArrayList(); /** * Handles events coming from the phone stack. Overridden from handler. * * @param msg the message to handle */ @Override public void handleMessage(Message msg) { AsyncResult ar; switch (msg.what) { case EVENT_NEW_SMS: // A new SMS has been received by the device if (false) { Log.d(TAG, "New SMS Message Received"); } SmsMessage sms; ar = (AsyncResult) msg.obj; if (ar.exception != null) { Log.e(TAG, "Exception processing incoming SMS. Exception:" + ar.exception); return; } sms = (SmsMessage) ar.result; try { int result = dispatchMessage(sms.mWrappedSmsMessage); if (result != Activity.RESULT_OK) { // RESULT_OK means that message was broadcast for app(s) to handle. // Any other result, we should ack here. boolean handled = (result == Intents.RESULT_SMS_HANDLED); notifyAndAcknowledgeLastIncomingSms(handled, result, null); } } catch (RuntimeException ex) { Log.e(TAG, "Exception dispatching message", ex); notifyAndAcknowledgeLastIncomingSms(false, Intents.RESULT_SMS_GENERIC_ERROR, null); } break; case EVENT_SEND_SMS_COMPLETE: // An outbound SMS has been successfully transferred, or failed. handleSendComplete((AsyncResult) msg.obj); break; case EVENT_SEND_RETRY: sendSms((SmsTracker) msg.obj); break; case EVENT_SEND_LIMIT_REACHED_CONFIRMATION: handleReachSentLimit((SmsTracker)(msg.obj)); break; case EVENT_SEND_CONFIRMED_SMS: { SmsTracker tracker = (SmsTracker) msg.obj; if (tracker.isMultipart()) { sendMultipartSms(tracker); } else { sendSms(tracker); } mPendingTrackerCount--; break; } case EVENT_STOP_SENDING: { SmsTracker tracker = (SmsTracker) msg.obj; if (tracker.mSentIntent != null) { try { tracker.mSentIntent.send(RESULT_ERROR_LIMIT_EXCEEDED); } catch (CanceledException ex) { Log.e(TAG, "failed to send RESULT_ERROR_LIMIT_EXCEEDED"); } } mPendingTrackerCount--; break; } } } private void createWakelock() { PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "SMSDispatcher"); mWakeLock.setReferenceCounted(true); } /** * Grabs a wake lock and sends intent as an ordered broadcast. * The resultReceiver will check for errors and ACK/NACK back * to the RIL. * * @param intent intent to broadcast * @param permission Receivers are required to have this permission */ public void dispatch(Intent intent, String permission) { // Hold a wake lock for WAKE_LOCK_TIMEOUT seconds, enough to give any // receivers time to take their own wake locks. mWakeLock.acquire(WAKE_LOCK_TIMEOUT); mContext.sendOrderedBroadcast(intent, permission, mResultReceiver, this, Activity.RESULT_OK, null, null); } /** * Called when SMS send completes. Broadcasts a sentIntent on success. * On failure, either sets up retries or broadcasts a sentIntent with * the failure in the result code. * * @param ar AsyncResult passed into the message handler. ar.result should * an SmsResponse instance if send was successful. ar.userObj * should be an SmsTracker instance. */ protected void handleSendComplete(AsyncResult ar) { SmsTracker tracker = (SmsTracker) ar.userObj; PendingIntent sentIntent = tracker.mSentIntent; if (ar.exception == null) { if (false) { Log.d(TAG, "SMS send complete. Broadcasting " + "intent: " + sentIntent); } if (tracker.mDeliveryIntent != null) { // Expecting a status report. Add it to the list. int messageRef = ((SmsResponse)ar.result).messageRef; tracker.mMessageRef = messageRef; deliveryPendingList.add(tracker); } if (sentIntent != null) { try { if (mRemainingMessages > -1) { mRemainingMessages--; } if (mRemainingMessages == 0) { Intent sendNext = new Intent(); sendNext.putExtra(SEND_NEXT_MSG_EXTRA, true); sentIntent.send(mContext, Activity.RESULT_OK, sendNext); } else { sentIntent.send(Activity.RESULT_OK); } } catch (CanceledException ex) {} } } else { if (false) { Log.d(TAG, "SMS send failed"); } int ss = mPhone.getServiceState().getState(); if (ss != ServiceState.STATE_IN_SERVICE) { handleNotInService(ss, tracker.mSentIntent); } else if ((((CommandException)(ar.exception)).getCommandError() == CommandException.Error.SMS_FAIL_RETRY) && tracker.mRetryCount < MAX_SEND_RETRIES) { // Retry after a delay if needed. // TODO: According to TS 23.040, 9.2.3.6, we should resend // with the same TP-MR as the failed message, and // TP-RD set to 1. However, we don't have a means of // knowing the MR for the failed message (EF_SMSstatus // may or may not have the MR corresponding to this // message, depending on the failure). Also, in some // implementations this retry is handled by the baseband. tracker.mRetryCount++; Message retryMsg = obtainMessage(EVENT_SEND_RETRY, tracker); sendMessageDelayed(retryMsg, SEND_RETRY_DELAY); } else if (tracker.mSentIntent != null) { int error = RESULT_ERROR_GENERIC_FAILURE; if (((CommandException)(ar.exception)).getCommandError() == CommandException.Error.FDN_CHECK_FAILURE) { error = RESULT_ERROR_FDN_CHECK_FAILURE; } // Done retrying; return an error to the app. try { Intent fillIn = new Intent(); if (ar.result != null) { fillIn.putExtra("errorCode", ((SmsResponse)ar.result).errorCode); } if (mRemainingMessages > -1) { mRemainingMessages--; } if (mRemainingMessages == 0) { fillIn.putExtra(SEND_NEXT_MSG_EXTRA, true); } tracker.mSentIntent.send(mContext, error, fillIn); } catch (CanceledException ex) {} } } } /** * Handles outbound message when the phone is not in service. * * @param ss Current service state. Valid values are: * OUT_OF_SERVICE * EMERGENCY_ONLY * POWER_OFF * @param sentIntent the PendingIntent to send the error to */ protected static void handleNotInService(int ss, PendingIntent sentIntent) { if (sentIntent != null) { try { if (ss == ServiceState.STATE_POWER_OFF) { sentIntent.send(RESULT_ERROR_RADIO_OFF); } else { sentIntent.send(RESULT_ERROR_NO_SERVICE); } } catch (CanceledException ex) {} } } /** * Dispatches an incoming SMS messages. * * @param sms the incoming message from the phone * @return a result code from {@link Telephony.Sms.Intents}, or * {@link Activity#RESULT_OK} if the message has been broadcast * to applications */ public abstract int dispatchMessage(SmsMessageBase sms); /** * Dispatch a normal incoming SMS. This is called from the format-specific * {@link #dispatchMessage(SmsMessageBase)} if no format-specific handling is required. * * @param sms * @return */ protected int dispatchNormalMessage(SmsMessageBase sms) { SmsHeader smsHeader = sms.getUserDataHeader(); // See if message is partial or port addressed. if ((smsHeader == null) || (smsHeader.concatRef == null)) { // Message is not partial (not part of concatenated sequence). byte[][] pdus = new byte[1][]; pdus[0] = sms.getPdu(); if (smsHeader != null && smsHeader.portAddrs != null) { if (smsHeader.portAddrs.destPort == SmsHeader.PORT_WAP_PUSH) { // GSM-style WAP indication return mWapPush.dispatchWapPdu(sms.getUserData()); } else { // The message was sent to a port, so concoct a URI for it. dispatchPortAddressedPdus(pdus, smsHeader.portAddrs.destPort); } } else { // Normal short and non-port-addressed message, dispatch it. dispatchPdus(pdus); } return Activity.RESULT_OK; } else { // Process the message part. SmsHeader.ConcatRef concatRef = smsHeader.concatRef; SmsHeader.PortAddrs portAddrs = smsHeader.portAddrs; return processMessagePart(sms.getPdu(), sms.getOriginatingAddress(), concatRef.refNumber, concatRef.seqNumber, concatRef.msgCount, sms.getTimestampMillis(), (portAddrs != null ? portAddrs.destPort : -1), false); } } /** * If this is the last part send the parts out to the application, otherwise * the part is stored for later processing. Handles both 3GPP concatenated messages * as well as 3GPP2 format WAP push messages processed by * {@link com.android.internal.telephony.cdma.CdmaSMSDispatcher#processCdmaWapPdu}. * * @param pdu the message PDU, or the datagram portion of a CDMA WDP datagram segment * @param address the originating address * @param referenceNumber distinguishes concatenated messages from the same sender * @param sequenceNumber the order of this segment in the message * (starting at 0 for CDMA WDP datagrams and 1 for concatenated messages). * @param messageCount the number of segments in the message * @param timestamp the service center timestamp in millis * @param destPort the destination port for the message, or -1 for no destination port * @param isCdmaWapPush true if pdu is a CDMA WDP datagram segment and not an SM PDU * * @return a result code from {@link Telephony.Sms.Intents}, or * {@link Activity#RESULT_OK} if the message has been broadcast * to applications */ protected int processMessagePart(byte[] pdu, String address, int referenceNumber, int sequenceNumber, int messageCount, long timestamp, int destPort, boolean isCdmaWapPush) { byte[][] pdus = null; Cursor cursor = null; try { // used by several query selection arguments String refNumber = Integer.toString(referenceNumber); String seqNumber = Integer.toString(sequenceNumber); // Check for duplicate message segment cursor = mResolver.query(mRawUri, PDU_PROJECTION, "address=? AND reference_number=? AND sequence=?", new String[] {address, refNumber, seqNumber}, null); // moveToNext() returns false if no duplicates were found if (cursor.moveToNext()) { Log.w(TAG, "Discarding duplicate message segment from address=" + address + " refNumber=" + refNumber + " seqNumber=" + seqNumber); String oldPduString = cursor.getString(PDU_COLUMN); byte[] oldPdu = HexDump.hexStringToByteArray(oldPduString); if (!Arrays.equals(oldPdu, pdu)) { Log.e(TAG, "Warning: dup message segment PDU of length " + pdu.length + " is different from existing PDU of length " + oldPdu.length); } return Intents.RESULT_SMS_HANDLED; } cursor.close(); // not a dup, query for all other segments of this concatenated message String where = "address=? AND reference_number=?"; String[] whereArgs = new String[] {address, refNumber}; cursor = mResolver.query(mRawUri, PDU_SEQUENCE_PORT_PROJECTION, where, whereArgs, null); int cursorCount = cursor.getCount(); if (cursorCount != messageCount - 1) { // We don't have all the parts yet, store this one away ContentValues values = new ContentValues(); values.put("date", timestamp); values.put("pdu", HexDump.toHexString(pdu)); values.put("address", address); values.put("reference_number", referenceNumber); values.put("count", messageCount); values.put("sequence", sequenceNumber); if (destPort != -1) { values.put("destination_port", destPort); } mResolver.insert(mRawUri, values); return Intents.RESULT_SMS_HANDLED; } // All the parts are in place, deal with them pdus = new byte[messageCount][]; for (int i = 0; i < cursorCount; i++) { cursor.moveToNext(); int cursorSequence = cursor.getInt(SEQUENCE_COLUMN); // GSM sequence numbers start at 1; CDMA WDP datagram sequence numbers start at 0 if (!isCdmaWapPush) { cursorSequence--; } pdus[cursorSequence] = HexDump.hexStringToByteArray( cursor.getString(PDU_COLUMN)); // Read the destination port from the first segment (needed for CDMA WAP PDU). // It's not a bad idea to prefer the port from the first segment for 3GPP as well. if (cursorSequence == 0 && !cursor.isNull(DESTINATION_PORT_COLUMN)) { destPort = cursor.getInt(DESTINATION_PORT_COLUMN); } } // This one isn't in the DB, so add it // GSM sequence numbers start at 1; CDMA WDP datagram sequence numbers start at 0 if (isCdmaWapPush) { pdus[sequenceNumber] = pdu; } else { pdus[sequenceNumber - 1] = pdu; } // Remove the parts from the database mResolver.delete(mRawUri, where, whereArgs); } catch (SQLException e) { Log.e(TAG, "Can't access multipart SMS database", e); return Intents.RESULT_SMS_GENERIC_ERROR; } finally { if (cursor != null) cursor.close(); } // Special handling for CDMA WDP datagrams if (isCdmaWapPush) { // Build up the data stream ByteArrayOutputStream output = new ByteArrayOutputStream(); for (int i = 0; i < messageCount; i++) { // reassemble the (WSP-)pdu output.write(pdus[i], 0, pdus[i].length); } byte[] datagram = output.toByteArray(); // Dispatch the PDU to applications if (destPort == SmsHeader.PORT_WAP_PUSH) { // Handle the PUSH return mWapPush.dispatchWapPdu(datagram); } else { pdus = new byte[1][]; pdus[0] = datagram; // The messages were sent to any other WAP port dispatchPortAddressedPdus(pdus, destPort); return Activity.RESULT_OK; } } // Dispatch the PDUs to applications if (destPort != -1) { if (destPort == SmsHeader.PORT_WAP_PUSH) { // Build up the data stream ByteArrayOutputStream output = new ByteArrayOutputStream(); for (int i = 0; i < messageCount; i++) { SmsMessage msg = SmsMessage.createFromPdu(pdus[i], getFormat()); byte[] data = msg.getUserData(); output.write(data, 0, data.length); } // Handle the PUSH return mWapPush.dispatchWapPdu(output.toByteArray()); } else { // The messages were sent to a port, so concoct a URI for it dispatchPortAddressedPdus(pdus, destPort); } } else { // The messages were not sent to a port dispatchPdus(pdus); } return Activity.RESULT_OK; } /** * Dispatches standard PDUs to interested applications * * @param pdus The raw PDUs making up the message */ protected void dispatchPdus(byte[][] pdus) { Intent intent = new Intent(Intents.SMS_RECEIVED_ACTION); intent.putExtra("pdus", pdus); intent.putExtra("format", getFormat()); dispatch(intent, RECEIVE_SMS_PERMISSION); } /** * Dispatches port addressed PDUs to interested applications * * @param pdus The raw PDUs making up the message * @param port The destination port of the messages */ protected void dispatchPortAddressedPdus(byte[][] pdus, int port) { Uri uri = Uri.parse("sms://localhost:" + port); Intent intent = new Intent(Intents.DATA_SMS_RECEIVED_ACTION, uri); intent.putExtra("pdus", pdus); intent.putExtra("format", getFormat()); dispatch(intent, RECEIVE_SMS_PERMISSION); } /** * Send a data based SMS to a specific application port. * * @param destAddr the address to send the message to * @param scAddr is the service center address or null to use * the current default SMSC * @param destPort the port to deliver the message to * @param data the body of the message to send * @param sentIntent if not NULL this PendingIntent is * broadcast when the message is successfully sent, or failed. * The result code will be Activity.RESULT_OK for success, * or one of these errors:
* RESULT_ERROR_GENERIC_FAILURE
* RESULT_ERROR_RADIO_OFF
* RESULT_ERROR_NULL_PDU
* RESULT_ERROR_NO_SERVICE
. * For RESULT_ERROR_GENERIC_FAILURE the sentIntent may include * the extra "errorCode" containing a radio technology specific value, * generally only useful for troubleshooting.
* 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 PendingIntent is * broadcast when the message is delivered to the recipient. The * raw pdu of the status report is in the extended data ("pdu"). */ protected abstract void sendData(String destAddr, String scAddr, int destPort, byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent); /** * Send a text based SMS. * * @param destAddr the address to send the message to * @param scAddr 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 PendingIntent is * broadcast when the message is successfully sent, or failed. * The result code will be Activity.RESULT_OK for success, * or one of these errors:
* RESULT_ERROR_GENERIC_FAILURE
* RESULT_ERROR_RADIO_OFF
* RESULT_ERROR_NULL_PDU
* RESULT_ERROR_NO_SERVICE
. * For RESULT_ERROR_GENERIC_FAILURE the sentIntent may include * the extra "errorCode" containing a radio technology specific value, * generally only useful for troubleshooting.
* 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 PendingIntent is * broadcast when the message is delivered to the recipient. The * raw pdu of the status report is in the extended data ("pdu"). */ protected abstract void sendText(String destAddr, String scAddr, String text, PendingIntent sentIntent, PendingIntent deliveryIntent); /** * 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 * @return TextEncodingDetails */ protected abstract TextEncodingDetails calculateLength(CharSequence messageBody, boolean use7bitOnly); /** * Send a multi-part text based SMS. * * @param destAddr the address to send the message to * @param scAddr is the service center address or null to use * the current default SMSC * @param parts an ArrayList of strings that, in order, * comprise the original message * @param sentIntents if not null, an ArrayList of * PendingIntents (one for each message part) that is * broadcast when the corresponding message part has been sent. * The result code will be Activity.RESULT_OK for success, * or one of these errors: * RESULT_ERROR_GENERIC_FAILURE * RESULT_ERROR_RADIO_OFF * RESULT_ERROR_NULL_PDU * RESULT_ERROR_NO_SERVICE. * 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 ArrayList of * PendingIntents (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"). */ protected void sendMultipartText(String destAddr, String scAddr, ArrayList parts, ArrayList sentIntents, ArrayList deliveryIntents) { int refNumber = getNextConcatenatedRef() & 0x00FF; int msgCount = parts.size(); int encoding = android.telephony.SmsMessage.ENCODING_UNKNOWN; mRemainingMessages = msgCount; TextEncodingDetails[] encodingForParts = new TextEncodingDetails[msgCount]; for (int i = 0; i < msgCount; i++) { TextEncodingDetails details = calculateLength(parts.get(i), false); if (encoding != details.codeUnitSize && (encoding == android.telephony.SmsMessage.ENCODING_UNKNOWN || encoding == android.telephony.SmsMessage.ENCODING_7BIT)) { encoding = details.codeUnitSize; } encodingForParts[i] = details; } for (int i = 0; i < msgCount; i++) { SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); concatRef.refNumber = refNumber; concatRef.seqNumber = i + 1; // 1-based sequence concatRef.msgCount = msgCount; // TODO: We currently set this to true since our messaging app will never // send more than 255 parts (it converts the message to MMS well before that). // However, we should support 3rd party messaging apps that might need 16-bit // references // Note: It's not sufficient to just flip this bit to true; it will have // ripple effects (several calculations assume 8-bit ref). concatRef.isEightBits = true; SmsHeader smsHeader = new SmsHeader(); smsHeader.concatRef = concatRef; // Set the national language tables for 3GPP 7-bit encoding, if enabled. if (encoding == android.telephony.SmsMessage.ENCODING_7BIT) { smsHeader.languageTable = encodingForParts[i].languageTable; smsHeader.languageShiftTable = encodingForParts[i].languageShiftTable; } PendingIntent sentIntent = null; if (sentIntents != null && sentIntents.size() > i) { sentIntent = sentIntents.get(i); } PendingIntent deliveryIntent = null; if (deliveryIntents != null && deliveryIntents.size() > i) { deliveryIntent = deliveryIntents.get(i); } sendNewSubmitPdu(destAddr, scAddr, parts.get(i), smsHeader, encoding, sentIntent, deliveryIntent, (i == (msgCount - 1))); } } /** * Create a new SubmitPdu and send it. */ protected abstract void sendNewSubmitPdu(String destinationAddress, String scAddress, String message, SmsHeader smsHeader, int encoding, PendingIntent sentIntent, PendingIntent deliveryIntent, boolean lastPart); /** * Send a SMS * * @param smsc the SMSC to send the message through, or NULL for the * default SMSC * @param pdu the raw PDU to send * @param sentIntent if not NULL this Intent is * broadcast when the message is successfully sent, or failed. * The result code will be Activity.RESULT_OK for success, * or one of these errors: * RESULT_ERROR_GENERIC_FAILURE * RESULT_ERROR_RADIO_OFF * RESULT_ERROR_NULL_PDU * RESULT_ERROR_NO_SERVICE. * 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 Intent is * broadcast when the message is delivered to the recipient. The * raw pdu of the status report is in the extended data ("pdu"). * @param destAddr the destination phone number (for short code confirmation) */ protected void sendRawPdu(byte[] smsc, byte[] pdu, PendingIntent sentIntent, PendingIntent deliveryIntent, String destAddr) { if (mSmsSendDisabled) { if (sentIntent != null) { try { sentIntent.send(RESULT_ERROR_NO_SERVICE); } catch (CanceledException ex) {} } Log.d(TAG, "Device does not support sending sms."); return; } if (pdu == null) { if (sentIntent != null) { try { sentIntent.send(RESULT_ERROR_NULL_PDU); } catch (CanceledException ex) {} } return; } HashMap map = new HashMap(); map.put("smsc", smsc); map.put("pdu", pdu); // Get calling app package name via UID from Binder call PackageManager pm = mContext.getPackageManager(); String[] packageNames = pm.getPackagesForUid(Binder.getCallingUid()); if (packageNames == null || packageNames.length == 0) { // Refuse to send SMS if we can't get the calling package name. Log.e(TAG, "Can't get calling app package name: refusing to send SMS"); if (sentIntent != null) { try { sentIntent.send(RESULT_ERROR_GENERIC_FAILURE); } catch (CanceledException ex) { Log.e(TAG, "failed to send error result"); } } return; } String appPackage = packageNames[0]; // Strip non-digits from destination phone number before checking for short codes // and before displaying the number to the user if confirmation is required. SmsTracker tracker = new SmsTracker(map, sentIntent, deliveryIntent, appPackage, PhoneNumberUtils.extractNetworkPortion(destAddr)); // check for excessive outgoing SMS usage by this app if (!mUsageMonitor.check(appPackage, SINGLE_PART_SMS)) { sendMessage(obtainMessage(EVENT_SEND_LIMIT_REACHED_CONFIRMATION, tracker)); return; } int ss = mPhone.getServiceState().getState(); if (ss != ServiceState.STATE_IN_SERVICE) { handleNotInService(ss, tracker.mSentIntent); } else { sendSms(tracker); } } /** * Deny sending an SMS if the outgoing queue limit is reached. Used when the message * must be confirmed by the user due to excessive usage or potential premium SMS detected. * @param tracker the SmsTracker for the message to send * @return true if the message was denied; false to continue with send confirmation */ private boolean denyIfQueueLimitReached(SmsTracker tracker) { if (mPendingTrackerCount >= MO_MSG_QUEUE_LIMIT) { // Deny sending message when the queue limit is reached. try { tracker.mSentIntent.send(RESULT_ERROR_LIMIT_EXCEEDED); } catch (CanceledException ex) { Log.e(TAG, "failed to send back RESULT_ERROR_LIMIT_EXCEEDED"); } return true; } mPendingTrackerCount++; return false; } /** * Returns the label for the specified app package name. * @param appPackage the package name of the app requesting to send an SMS * @return the label for the specified app, or the package name if getApplicationInfo() fails */ private CharSequence getAppLabel(String appPackage) { PackageManager pm = mContext.getPackageManager(); try { ApplicationInfo appInfo = pm.getApplicationInfo(appPackage, 0); return appInfo.loadLabel(pm); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "PackageManager Name Not Found for package " + appPackage); return appPackage; // fall back to package name if we can't get app label } } /** * Post an alert when SMS needs confirmation due to excessive usage. * @param tracker an SmsTracker for the current message. */ protected void handleReachSentLimit(SmsTracker tracker) { if (denyIfQueueLimitReached(tracker)) { return; // queue limit reached; error was returned to caller } CharSequence appLabel = getAppLabel(tracker.mAppPackage); Resources r = Resources.getSystem(); Spanned messageText = Html.fromHtml(r.getString(R.string.sms_control_message, appLabel)); ConfirmDialogListener listener = new ConfirmDialogListener(tracker); AlertDialog d = new AlertDialog.Builder(mContext) .setTitle(R.string.sms_control_title) .setIcon(R.drawable.stat_sys_warning) .setMessage(messageText) .setPositiveButton(r.getString(R.string.sms_control_yes), listener) .setNegativeButton(r.getString(R.string.sms_control_no), listener) .setOnCancelListener(listener) .create(); d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); d.show(); } /** * Send the message along to the radio. * * @param tracker holds the SMS message to send */ protected abstract void sendSms(SmsTracker tracker); /** * Send the multi-part SMS based on multipart Sms tracker * * @param tracker holds the multipart Sms tracker ready to be sent */ private void sendMultipartSms(SmsTracker tracker) { ArrayList parts; ArrayList sentIntents; ArrayList deliveryIntents; HashMap map = tracker.mData; String destinationAddress = (String) map.get("destination"); String scAddress = (String) map.get("scaddress"); parts = (ArrayList) map.get("parts"); sentIntents = (ArrayList) map.get("sentIntents"); deliveryIntents = (ArrayList) map.get("deliveryIntents"); // check if in service int ss = mPhone.getServiceState().getState(); if (ss != ServiceState.STATE_IN_SERVICE) { for (int i = 0, count = parts.size(); i < count; i++) { PendingIntent sentIntent = null; if (sentIntents != null && sentIntents.size() > i) { sentIntent = sentIntents.get(i); } handleNotInService(ss, sentIntent); } return; } sendMultipartText(destinationAddress, scAddress, parts, sentIntents, deliveryIntents); } /** * Send an acknowledge message. * @param success indicates that last message was successfully received. * @param result result code indicating any error * @param response callback message sent when operation completes. */ protected abstract void acknowledgeLastIncomingSms(boolean success, int result, Message response); /** * Notify interested apps if the framework has rejected an incoming SMS, * and send an acknowledge message to the network. * @param success indicates that last message was successfully received. * @param result result code indicating any error * @param response callback message sent when operation completes. */ private void notifyAndAcknowledgeLastIncomingSms(boolean success, int result, Message response) { if (!success) { // broadcast SMS_REJECTED_ACTION intent Intent intent = new Intent(Intents.SMS_REJECTED_ACTION); intent.putExtra("result", result); mWakeLock.acquire(WAKE_LOCK_TIMEOUT); mContext.sendBroadcast(intent, "android.permission.RECEIVE_SMS"); } acknowledgeLastIncomingSms(success, result, response); } /** * Keeps track of an SMS that has been sent to the RIL, until it has * successfully been sent, or we're done trying. * */ protected static final class SmsTracker { // fields need to be public for derived SmsDispatchers public final HashMap mData; public int mRetryCount; public int mMessageRef; public final PendingIntent mSentIntent; public final PendingIntent mDeliveryIntent; public final String mAppPackage; public final String mDestAddress; public SmsTracker(HashMap data, PendingIntent sentIntent, PendingIntent deliveryIntent, String appPackage, String destAddr) { mData = data; mSentIntent = sentIntent; mDeliveryIntent = deliveryIntent; mRetryCount = 0; mAppPackage = appPackage; mDestAddress = destAddr; } /** * Returns whether this tracker holds a multi-part SMS. * @return true if the tracker holds a multi-part SMS; false otherwise */ protected boolean isMultipart() { HashMap map = mData; return map.containsKey("parts"); } } /** * Dialog listener for SMS confirmation dialog. */ private final class ConfirmDialogListener implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener { private final SmsTracker mTracker; ConfirmDialogListener(SmsTracker tracker) { mTracker = tracker; } @Override public void onClick(DialogInterface dialog, int which) { if (which == DialogInterface.BUTTON_POSITIVE) { Log.d(TAG, "CONFIRM sending SMS"); sendMessage(obtainMessage(EVENT_SEND_CONFIRMED_SMS, mTracker)); } else if (which == DialogInterface.BUTTON_NEGATIVE) { Log.d(TAG, "DENY sending SMS"); sendMessage(obtainMessage(EVENT_STOP_SENDING, mTracker)); } } @Override public void onCancel(DialogInterface dialog) { Log.d(TAG, "dialog dismissed: don't send SMS"); sendMessage(obtainMessage(EVENT_STOP_SENDING, mTracker)); } } private final BroadcastReceiver mResultReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // Assume the intent is one of the SMS receive intents that // was sent as an ordered broadcast. Check result and ACK. int rc = getResultCode(); boolean success = (rc == Activity.RESULT_OK) || (rc == Intents.RESULT_SMS_HANDLED); // For a multi-part message, this only ACKs the last part. // Previous parts were ACK'd as they were received. acknowledgeLastIncomingSms(success, rc, null); } }; protected void dispatchBroadcastMessage(SmsCbMessage message) { if (message.isEmergencyMessage()) { Intent intent = new Intent(Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION); intent.putExtra("message", message); Log.d(TAG, "Dispatching emergency SMS CB"); dispatch(intent, RECEIVE_EMERGENCY_BROADCAST_PERMISSION); } else { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); intent.putExtra("message", message); Log.d(TAG, "Dispatching SMS CB"); dispatch(intent, RECEIVE_SMS_PERMISSION); } } }