/* * 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.PendingIntent; import android.app.AlertDialog; import android.app.PendingIntent.CanceledException; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.DialogInterface; import android.content.IntentFilter; import android.content.res.Resources; import android.database.Cursor; import android.database.SQLException; import android.net.Uri; import android.os.AsyncResult; import android.os.Handler; import android.os.Message; import android.os.PowerManager; import android.provider.Telephony; import android.provider.Telephony.Sms.Intents; import android.provider.Settings; import android.telephony.SmsMessage; import android.telephony.ServiceState; import android.util.Config; import android.util.Log; import android.view.WindowManager; import com.android.internal.telephony.CommandsInterface; import com.android.internal.telephony.SmsMessageBase; import com.android.internal.telephony.SmsResponse; import com.android.internal.telephony.WapPushOverSms; import com.android.internal.util.HexDump; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Random; import com.android.internal.R; import static android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE; 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; import static android.telephony.SmsManager.RESULT_ERROR_LIMIT_EXCEEDED; public abstract class SMSDispatcher extends Handler { private static final String TAG = "SMS"; /** Default checking period for SMS sent without user permit */ private static final int DEFAULT_SMS_CHECK_PERIOD = 3600000; /** Default number of SMS sent in checking period without user permit */ private static final int DEFAULT_SMS_MAX_COUNT = 100; /** Default timeout for SMS sent query */ private static final int DEFAULT_SMS_TIMOUEOUT = 6000; protected static final String[] RAW_PROJECTION = new String[] { "pdu", "sequence", "destination_port", }; static final int MAIL_SEND_SMS = 1; static final protected int EVENT_NEW_SMS = 1; static final protected int EVENT_SEND_SMS_COMPLETE = 2; /** Retry sending a previously failed SMS message */ static final protected int EVENT_SEND_RETRY = 3; /** Status report received */ static final protected int EVENT_NEW_SMS_STATUS_REPORT = 5; /** SIM/RUIM storage is full */ static final protected int EVENT_ICC_FULL = 6; /** SMS confirm required */ static final protected int EVENT_POST_ALERT = 7; /** Send the user confirmed SMS */ static final protected int EVENT_SEND_CONFIRMED_SMS = 8; /** Alert is timeout */ static final protected int EVENT_ALERT_TIMEOUT = 9; /** Stop the sending */ static final protected int EVENT_STOP_SENDING = 10; /** Memory status reporting is acknowledged by RIL */ static final protected int EVENT_REPORT_MEMORY_STATUS_DONE = 11; /** Radio is ON */ static final protected int EVENT_RADIO_ON = 12; protected Phone mPhone; protected Context mContext; protected ContentResolver mResolver; protected CommandsInterface mCm; protected final WapPushOverSms mWapPush; protected 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. */ private static int sConcatenatedRef; private SmsCounter mCounter; private ArrayList mSTrackers = new ArrayList(MO_MSG_QUEUE_LIMIT); /** 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 final int WAKE_LOCK_TIMEOUT = 5000; private static SmsMessage mSmsMessage; private static SmsMessageBase mSmsMessageBase; private SmsMessageBase.SubmitPduBase mSubmitPduBase; protected boolean mStorageAvailable = true; protected boolean mReportMemoryStatusPending = false; protected static int getNextConcatenatedRef() { sConcatenatedRef += 1; return sConcatenatedRef; } /** * Implement the per-application based SMS control, which only allows * a limit on the number of SMS/MMS messages an app can send in checking * period. */ private class SmsCounter { private int mCheckPeriod; private int mMaxAllowed; private HashMap<String, ArrayList<Long>> mSmsStamp; /** * Create SmsCounter * @param mMax is the number of SMS allowed without user permit * @param mPeriod is the checking period */ SmsCounter(int mMax, int mPeriod) { mMaxAllowed = mMax; mCheckPeriod = mPeriod; mSmsStamp = new HashMap<String, ArrayList<Long>> (); } /** * Check to see if an application allow to send new SMS messages * * @param appName is the application sending sms * @param smsWaiting is the number of new sms wants to be sent * @return true if application is allowed to send the requested number * of new sms messages */ boolean check(String appName, int smsWaiting) { if (!mSmsStamp.containsKey(appName)) { mSmsStamp.put(appName, new ArrayList<Long>()); } return isUnderLimit(mSmsStamp.get(appName), smsWaiting); } private boolean isUnderLimit(ArrayList<Long> sent, int smsWaiting) { Long ct = System.currentTimeMillis(); Log.d(TAG, "SMS send size=" + sent.size() + "time=" + ct); while (sent.size() > 0 && (ct - sent.get(0)) > mCheckPeriod ) { sent.remove(0); } if ( (sent.size() + smsWaiting) <= mMaxAllowed) { for (int i = 0; i < smsWaiting; i++ ) { sent.add(ct); } return true; } return false; } } protected SMSDispatcher(PhoneBase phone) { mPhone = phone; mWapPush = new WapPushOverSms(phone, this); mContext = phone.getContext(); mResolver = mContext.getContentResolver(); mCm = phone.mCM; createWakelock(); int check_period = Settings.Secure.getInt(mResolver, Settings.Secure.SMS_OUTGOING_CHECK_INTERVAL_MS, DEFAULT_SMS_CHECK_PERIOD); int max_count = Settings.Secure.getInt(mResolver, Settings.Secure.SMS_OUTGOING_CHECK_MAX_COUNT, DEFAULT_SMS_MAX_COUNT); mCounter = new SmsCounter(max_count, check_period); mCm.setOnNewSMS(this, EVENT_NEW_SMS, null); mCm.setOnSmsStatus(this, EVENT_NEW_SMS_STATUS_REPORT, null); mCm.setOnIccSmsFull(this, EVENT_ICC_FULL, null); mCm.registerForOn(this, EVENT_RADIO_ON, null); // Don't always start message ref at 0. sConcatenatedRef = new Random().nextInt(256); // Register for device storage intents. Use these to notify the RIL // that storage for SMS is or is not available. // TODO: Revisit this for a later release. Storage reporting should // rely more on application indication. IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW); filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); mContext.registerReceiver(mResultReceiver, filter); } public void dispose() { mCm.unSetOnNewSMS(this); mCm.unSetOnSmsStatus(this); mCm.unSetOnIccSmsFull(this); mCm.unregisterForOn(this); } 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<SmsTracker> deliveryPendingList = new ArrayList<SmsTracker>(); /** * 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 (Config.LOGD) { 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_NEW_SMS_STATUS_REPORT: handleStatusReport((AsyncResult)msg.obj); break; case EVENT_ICC_FULL: handleIccFull(); break; case EVENT_POST_ALERT: handleReachSentLimit((SmsTracker)(msg.obj)); break; case EVENT_ALERT_TIMEOUT: ((AlertDialog)(msg.obj)).dismiss(); msg.obj = null; if (mSTrackers.isEmpty() == false) { try { SmsTracker sTracker = (SmsTracker)mSTrackers.remove(0); sTracker.mSentIntent.send(RESULT_ERROR_LIMIT_EXCEEDED); } catch (CanceledException ex) { Log.e(TAG, "failed to send back RESULT_ERROR_LIMIT_EXCEEDED"); } } if (Config.LOGD) { Log.d(TAG, "EVENT_ALERT_TIMEOUT, message stop sending"); } break; case EVENT_SEND_CONFIRMED_SMS: if (mSTrackers.isEmpty() == false) { SmsTracker sTracker = (SmsTracker)mSTrackers.remove(mSTrackers.size() - 1); if (isMultipartTracker(sTracker)) { sendMultipartSms(sTracker); } else { sendSms(sTracker); } removeMessages(EVENT_ALERT_TIMEOUT, msg.obj); } break; case EVENT_STOP_SENDING: if (mSTrackers.isEmpty() == false) { // Remove the latest one. try { SmsTracker sTracker = (SmsTracker)mSTrackers.remove(mSTrackers.size() - 1); sTracker.mSentIntent.send(RESULT_ERROR_LIMIT_EXCEEDED); } catch (CanceledException ex) { Log.e(TAG, "failed to send back RESULT_ERROR_LIMIT_EXCEEDED"); } removeMessages(EVENT_ALERT_TIMEOUT, msg.obj); } break; case EVENT_REPORT_MEMORY_STATUS_DONE: ar = (AsyncResult)msg.obj; if (ar.exception != null) { mReportMemoryStatusPending = true; Log.v(TAG, "Memory status report to modem pending : mStorageAvailable = " + mStorageAvailable); } else { mReportMemoryStatusPending = false; } break; case EVENT_RADIO_ON: if (mReportMemoryStatusPending) { Log.v(TAG, "Sending pending memory status report : mStorageAvailable = " + mStorageAvailable); mCm.reportSmsMemoryStatus(mStorageAvailable, obtainMessage(EVENT_REPORT_MEMORY_STATUS_DONE)); } 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 */ 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 SIM_FULL message is received from the RIL. Notifies interested * parties that SIM storage for SMS messages is full. */ private void handleIccFull(){ // broadcast SIM_FULL intent Intent intent = new Intent(Intents.SIM_FULL_ACTION); mWakeLock.acquire(WAKE_LOCK_TIMEOUT); mContext.sendBroadcast(intent, "android.permission.RECEIVE_SMS"); } /** * Called when a status report is received. This should correspond to * a previously successful SEND. * * @param ar AsyncResult passed into the message handler. ar.result should * be a String representing the status report PDU, as ASCII hex. */ protected abstract void handleStatusReport(AsyncResult ar); /** * 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 (Config.LOGD) { 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 { sentIntent.send(Activity.RESULT_OK); } catch (CanceledException ex) {} } } else { if (Config.LOGD) { Log.d(TAG, "SMS send failed"); } int ss = mPhone.getServiceState().getState(); if (ss != ServiceState.STATE_IN_SERVICE) { handleNotInService(ss, tracker); } 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) { // Done retrying; return an error to the app. try { Intent fillIn = new Intent(); if (ar.result != null) { fillIn.putExtra("errorCode", ((SmsResponse)ar.result).errorCode); } tracker.mSentIntent.send(mContext, RESULT_ERROR_GENERIC_FAILURE, 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 tracker An SmsTracker for the current message. */ protected void handleNotInService(int ss, SmsTracker tracker) { if (tracker.mSentIntent != null) { try { if (ss == ServiceState.STATE_POWER_OFF) { tracker.mSentIntent.send(RESULT_ERROR_RADIO_OFF); } else { tracker.mSentIntent.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 */ protected abstract int dispatchMessage(SmsMessageBase sms); /** * If this is the last part send the parts out to the application, otherwise * the part is stored for later processing. * * NOTE: concatRef (naturally) needs to be non-null, but portAddrs can be null. * @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(SmsMessageBase sms, SmsHeader.ConcatRef concatRef, SmsHeader.PortAddrs portAddrs) { // Lookup all other related parts StringBuilder where = new StringBuilder("reference_number ="); where.append(concatRef.refNumber); where.append(" AND address = ?"); String[] whereArgs = new String[] {sms.getOriginatingAddress()}; byte[][] pdus = null; Cursor cursor = null; try { cursor = mResolver.query(mRawUri, RAW_PROJECTION, where.toString(), whereArgs, null); int cursorCount = cursor.getCount(); if (cursorCount != concatRef.msgCount - 1) { // We don't have all the parts yet, store this one away ContentValues values = new ContentValues(); values.put("date", new Long(sms.getTimestampMillis())); values.put("pdu", HexDump.toHexString(sms.getPdu())); values.put("address", sms.getOriginatingAddress()); values.put("reference_number", concatRef.refNumber); values.put("count", concatRef.msgCount); values.put("sequence", concatRef.seqNumber); if (portAddrs != null) { values.put("destination_port", portAddrs.destPort); } mResolver.insert(mRawUri, values); return Intents.RESULT_SMS_HANDLED; } // All the parts are in place, deal with them int pduColumn = cursor.getColumnIndex("pdu"); int sequenceColumn = cursor.getColumnIndex("sequence"); pdus = new byte[concatRef.msgCount][]; for (int i = 0; i < cursorCount; i++) { cursor.moveToNext(); int cursorSequence = (int)cursor.getLong(sequenceColumn); pdus[cursorSequence - 1] = HexDump.hexStringToByteArray( cursor.getString(pduColumn)); } // This one isn't in the DB, so add it pdus[concatRef.seqNumber - 1] = sms.getPdu(); // Remove the parts from the database mResolver.delete(mRawUri, where.toString(), whereArgs); } catch (SQLException e) { Log.e(TAG, "Can't access multipart SMS database", e); // TODO: Would OUT_OF_MEMORY be more appropriate? return Intents.RESULT_SMS_GENERIC_ERROR; } finally { if (cursor != null) cursor.close(); } /** * TODO(cleanup): The following code has duplicated logic with * the radio-specific dispatchMessage code, which is fragile, * in addition to being redundant. Instead, if this method * maybe returned the reassembled message (or just contents), * the following code (which is not really related to * reconstruction) could be better consolidated. */ // Dispatch the PDUs to applications if (portAddrs != null) { if (portAddrs.destPort == SmsHeader.PORT_WAP_PUSH) { // Build up the data stream ByteArrayOutputStream output = new ByteArrayOutputStream(); for (int i = 0; i < concatRef.msgCount; i++) { SmsMessage msg = SmsMessage.createFromPdu(pdus[i]); 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, portAddrs.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); dispatch(intent, "android.permission.RECEIVE_SMS"); } /** * 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); dispatch(intent, "android.permission.RECEIVE_SMS"); } /** * 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 <code>PendingIntent</code> is * broadcast when the message is sucessfully 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 applicaitons, * 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"). */ 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 <code>PendingIntent</code> is * broadcast when the message is sucessfully 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"). */ protected abstract void sendText(String destAddr, String scAddr, String text, PendingIntent sentIntent, PendingIntent deliveryIntent); /** * 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 <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: * <code>RESULT_ERROR_GENERIC_FAILURE</code> * <code>RESULT_ERROR_RADIO_OFF</code> * <code>RESULT_ERROR_NULL_PDU</code>. * The per-application based SMS control checks sentIntent. If sentIntent * is NULL the caller will be checked against all unknown applicaitons, * 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"). */ protected abstract void sendMultipartText(String destAddr, String scAddr, ArrayList<String> parts, ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents); /** * Send a SMS * * @param smsc the SMSC to send the message through, or NULL for the * defatult SMSC * @param pdu the raw PDU to send * @param sentIntent if not NULL this <code>Intent</code> is * broadcast when the message is sucessfully sent, or failed. * The result code will be <code>Activity.RESULT_OK<code> for success, * or one of these errors: * <code>RESULT_ERROR_GENERIC_FAILURE</code> * <code>RESULT_ERROR_RADIO_OFF</code> * <code>RESULT_ERROR_NULL_PDU</code>. * The per-application based SMS control checks sentIntent. If sentIntent * is NULL the caller will be checked against all unknown applicaitons, * which cause smaller number of SMS to be sent in checking period. * @param deliveryIntent if not NULL this <code>Intent</code> is * broadcast when the message is delivered to the recipient. The * raw pdu of the status report is in the extended data ("pdu"). */ protected void sendRawPdu(byte[] smsc, byte[] pdu, PendingIntent sentIntent, PendingIntent deliveryIntent) { if (pdu == null) { if (sentIntent != null) { try { sentIntent.send(RESULT_ERROR_NULL_PDU); } catch (CanceledException ex) {} } return; } HashMap<String, Object> map = new HashMap<String, Object>(); map.put("smsc", smsc); map.put("pdu", pdu); SmsTracker tracker = new SmsTracker(map, sentIntent, deliveryIntent); int ss = mPhone.getServiceState().getState(); if (ss != ServiceState.STATE_IN_SERVICE) { handleNotInService(ss, tracker); } else { String appName = getAppNameByIntent(sentIntent); if (mCounter.check(appName, SINGLE_PART_SMS)) { sendSms(tracker); } else { sendMessage(obtainMessage(EVENT_POST_ALERT, tracker)); } } } /** * Post an alert while SMS needs user confirm. * * An SmsTracker for the current message. */ protected void handleReachSentLimit(SmsTracker tracker) { if (mSTrackers.size() >= MO_MSG_QUEUE_LIMIT) { // Deny the sending 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; } Resources r = Resources.getSystem(); String appName = getAppNameByIntent(tracker.mSentIntent); AlertDialog d = new AlertDialog.Builder(mContext) .setTitle(r.getString(R.string.sms_control_title)) .setMessage(appName + " " + r.getString(R.string.sms_control_message)) .setPositiveButton(r.getString(R.string.sms_control_yes), mListener) .setNegativeButton(r.getString(R.string.sms_control_no), mListener) .create(); d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); d.show(); mSTrackers.add(tracker); sendMessageDelayed ( obtainMessage(EVENT_ALERT_TIMEOUT, d), DEFAULT_SMS_TIMOUEOUT); } protected String getAppNameByIntent(PendingIntent intent) { Resources r = Resources.getSystem(); return (intent != null) ? intent.getTargetPackage() : r.getString(R.string.sms_control_default_app_name); } /** * 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 */ protected abstract void sendMultipartSms (SmsTracker tracker); /** * Activate or deactivate cell broadcast SMS. * * @param activate * 0 = activate, 1 = deactivate * @param response * Callback message is empty on completion */ protected abstract void activateCellBroadcastSms(int activate, Message response); /** * Query the current configuration of cell broadcast SMS. * * @param response * Callback message contains the configuration from the modem on completion * @see #setCellBroadcastConfig */ protected abstract void getCellBroadcastSmsConfig(Message response); /** * Configure cell broadcast SMS. * * @param configValuesArray * The first element defines the number of triples that follow. * A triple is made up of the service category, the language identifier * and a boolean that specifies whether the category is set active. * @param response * Callback message is empty on completion */ protected abstract void setCellBroadcastConfig(int[] configValuesArray, Message response); /** * 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); } /** * Check if a SmsTracker holds multi-part Sms * * @param tracker a SmsTracker could hold a multi-part Sms * @return true for tracker holds Multi-parts Sms */ private boolean isMultipartTracker (SmsTracker tracker) { HashMap map = tracker.mData; return ( map.get("parts") != null); } /** * Keeps track of an SMS that has been sent to the RIL, until it it has * successfully been sent, or we're done trying. * */ static protected class SmsTracker { // fields need to be public for derived SmsDispatchers public HashMap mData; public int mRetryCount; public int mMessageRef; public PendingIntent mSentIntent; public PendingIntent mDeliveryIntent; SmsTracker(HashMap data, PendingIntent sentIntent, PendingIntent deliveryIntent) { mData = data; mSentIntent = sentIntent; mDeliveryIntent = deliveryIntent; mRetryCount = 0; } } protected SmsTracker SmsTrackerFactory(HashMap data, PendingIntent sentIntent, PendingIntent deliveryIntent) { return new SmsTracker(data, sentIntent, deliveryIntent); } private DialogInterface.OnClickListener mListener = new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { if (which == DialogInterface.BUTTON_POSITIVE) { Log.d(TAG, "click YES to send out sms"); sendMessage(obtainMessage(EVENT_SEND_CONFIRMED_SMS)); } else if (which == DialogInterface.BUTTON_NEGATIVE) { Log.d(TAG, "click NO to stop sending"); sendMessage(obtainMessage(EVENT_STOP_SENDING)); } } }; private BroadcastReceiver mResultReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_DEVICE_STORAGE_LOW)) { mStorageAvailable = false; mCm.reportSmsMemoryStatus(false, obtainMessage(EVENT_REPORT_MEMORY_STATUS_DONE)); } else if (intent.getAction().equals(Intent.ACTION_DEVICE_STORAGE_OK)) { mStorageAvailable = true; mCm.reportSmsMemoryStatus(true, obtainMessage(EVENT_REPORT_MEMORY_STATUS_DONE)); } else { // 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); } } }; }