blob: 7efaa2eb4b35bcd54c183b391fdf9b9f3fc3fcf2 [file] [log] [blame]
/*
* 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;
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;
private boolean mStorageAvailable = true;
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.Gservices.getInt(mResolver,
Settings.Gservices.SMS_OUTGOING_CHECK_INTERVAL_MS,
DEFAULT_SMS_CHECK_PERIOD);
int max_count = Settings.Gservices.getInt(mResolver,
Settings.Gservices.SMS_OUTGOING_CEHCK_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);
// 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);
}
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 {
if (mStorageAvailable) {
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);
acknowledgeLastIncomingSms(handled, result, null);
}
} else {
acknowledgeLastIncomingSms(false, Intents.RESULT_SMS_OUT_OF_MEMORY, null);
}
} catch (RuntimeException ex) {
acknowledgeLastIncomingSms(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;
}
}
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);
/**
* 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, null);
} else if (intent.getAction().equals(Intent.ACTION_DEVICE_STORAGE_OK)) {
mStorageAvailable = true;
mCm.reportSmsMemoryStatus(true, null);
} 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);
}
}
};
}