| /* |
| * 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; |
| import static android.telephony.SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE; |
| |
| |
| 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) { |
| 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); |
| } |
| 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 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); |
| } |
| } |
| |
| }; |
| } |