Files
frameworks_base/telephony/java/com/android/internal/telephony/SMSDispatcher.java
Jake Hamby 929ca5e497 Fix bug in CDMA WDP datagram handling (fixes incoming MMS).
CDMA WDP datagram handling was refactored to use the same method
that handles concatenated SMS messages. WDP datagram sequence numbers
start at 0, but GSM/CDMA concatenated sequence numbers start at 1.
Changed SMSDispatcher.processMessagePart() to count from 0 when
handling WDP datagrams.

Also changed CdmaSMSDispatcher.processCdmaWapPdu() to correctly
decode segment numbers > 127 (signed byte conversion bug)
and to reject PDUs with an out-of-range segment number (invalid
ranges are already rejected for regular concatenated messages).

Bug: 5433331
Change-Id: I25c9567769de8edca789c0d1707d4916a4c46885
2011-10-11 19:15:32 -07:00

1095 lines
45 KiB
Java

/*
* 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.os.SystemProperties;
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.Log;
import android.view.WindowManager;
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 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 {
static final String TAG = "SMS"; // accessed from inner class
private static final String SEND_NEXT_MSG_EXTRA = "SendNextMsg";
/** Default timeout for SMS sent query */
private static final int DEFAULT_SMS_TIMEOUT = 6000;
/** 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";
/** 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;
/** SMS confirm required */
private static final int EVENT_POST_ALERT = 4;
/** Send the user confirmed SMS */
static final int EVENT_SEND_CONFIRMED_SMS = 5; // accessed from inner class
/** Alert is timeout */
private static final int EVENT_ALERT_TIMEOUT = 6;
/** Stop the sending */
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 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;
private final ArrayList<SmsTracker> mSTrackers = new ArrayList<SmsTracker>(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 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;
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<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 (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_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 = 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 (false) {
Log.d(TAG, "EVENT_ALERT_TIMEOUT, message stop sending");
}
break;
case EVENT_SEND_CONFIRMED_SMS:
if (mSTrackers.isEmpty() == false) {
SmsTracker sTracker = mSTrackers.remove(mSTrackers.size() - 1);
if (sTracker.isMultipart()) {
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 = 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;
}
}
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 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);
} 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 tracker An SmsTracker for the current message.
*/
protected static 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
*/
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 <code>PendingIntent</code> is
* broadcast when the message is successfully sent, or failed.
* The result code will be <code>Activity.RESULT_OK<code> for success,
* or one of these errors:<br>
* <code>RESULT_ERROR_GENERIC_FAILURE</code><br>
* <code>RESULT_ERROR_RADIO_OFF</code><br>
* <code>RESULT_ERROR_NULL_PDU</code><br>
* <code>RESULT_ERROR_NO_SERVICE</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 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 successfully sent, or failed.
* The result code will be <code>Activity.RESULT_OK<code> for success,
* or one of these errors:<br>
* <code>RESULT_ERROR_GENERIC_FAILURE</code><br>
* <code>RESULT_ERROR_RADIO_OFF</code><br>
* <code>RESULT_ERROR_NULL_PDU</code><br>
* <code>RESULT_ERROR_NO_SERVICE</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);
/**
* 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 <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>
* <code>RESULT_ERROR_NO_SERVICE</code>.
* The per-application based SMS control checks sentIntent. If sentIntent
* is NULL the caller will be checked against all unknown applications,
* which cause smaller number of SMS to be sent in checking period.
* @param deliveryIntents if not null, an <code>ArrayList</code> of
* <code>PendingIntent</code>s (one for each message part) that is
* broadcast when the corresponding message part has been delivered
* to the recipient. The raw pdu of the status report is in the
* extended data ("pdu").
*/
protected void sendMultipartText(String destAddr, String scAddr,
ArrayList<String> parts, ArrayList<PendingIntent> sentIntents,
ArrayList<PendingIntent> 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 <code>Intent</code> is
* broadcast when the message is successfully sent, or failed.
* The result code will be <code>Activity.RESULT_OK<code> for success,
* or one of these errors:
* <code>RESULT_ERROR_GENERIC_FAILURE</code>
* <code>RESULT_ERROR_RADIO_OFF</code>
* <code>RESULT_ERROR_NULL_PDU</code>
* <code>RESULT_ERROR_NO_SERVICE</code>.
* 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>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 (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<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 (mUsageMonitor.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_TIMEOUT);
}
protected static 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
*/
private void sendMultipartSms(SmsTracker tracker) {
ArrayList<String> parts;
ArrayList<PendingIntent> sentIntents;
ArrayList<PendingIntent> deliveryIntents;
HashMap<String, Object> map = tracker.mData;
String destinationAddress = (String) map.get("destination");
String scAddress = (String) map.get("scaddress");
parts = (ArrayList<String>) map.get("parts");
sentIntents = (ArrayList<PendingIntent>) map.get("sentIntents");
deliveryIntents = (ArrayList<PendingIntent>) 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, new SmsTracker(null, sentIntent, null));
}
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<String, Object> mData;
public int mRetryCount;
public int mMessageRef;
public final PendingIntent mSentIntent;
public final PendingIntent mDeliveryIntent;
public SmsTracker(HashMap<String, Object> data, PendingIntent sentIntent,
PendingIntent deliveryIntent) {
mData = data;
mSentIntent = sentIntent;
mDeliveryIntent = deliveryIntent;
mRetryCount = 0;
}
/**
* 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");
}
}
private final 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 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 dispatchBroadcastPdus(byte[][] pdus, boolean isEmergencyMessage) {
if (isEmergencyMessage) {
Intent intent = new Intent(Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION);
intent.putExtra("pdus", pdus);
Log.d(TAG, "Dispatching " + pdus.length + " emergency SMS CB pdus");
dispatch(intent, RECEIVE_EMERGENCY_BROADCAST_PERMISSION);
} else {
Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION);
intent.putExtra("pdus", pdus);
Log.d(TAG, "Dispatching " + pdus.length + " SMS CB pdus");
dispatch(intent, RECEIVE_SMS_PERMISSION);
}
}
}