/* * Copyright (C) 2007-2008 Esmertec AG. * Copyright (C) 2007-2008 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.mms.transaction; import static android.content.Intent.ACTION_BOOT_COMPLETED; import static android.provider.Telephony.Sms.Intents.SMS_RECEIVED_ACTION; import com.android.mms.MmsApp; import com.android.mms.ui.ClassZeroActivity; import com.android.mms.util.SendingProgressTokenManager; import com.google.android.mms.MmsException; import com.google.android.mms.util.SqliteWrapper; import android.app.Activity; import android.app.Service; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.Process; import android.provider.Telephony.Sms; import android.provider.Telephony.Sms.Inbox; import android.provider.Telephony.Sms.Intents; import android.provider.Telephony.Sms.Outbox; import android.telephony.ServiceState; import android.telephony.SmsManager; import android.telephony.SmsMessage; import android.util.Log; import android.widget.Toast; import com.android.internal.telephony.TelephonyIntents; import com.android.mms.R; import com.android.mms.ui.ClassZeroActivity; import com.android.mms.util.SendingProgressTokenManager; import com.google.android.mms.MmsException; import com.google.android.mms.util.SqliteWrapper; /** * This service essentially plays the role of a "worker thread", allowing us to store * incoming messages to the database, update notifications, etc. without blocking the * main thread that SmsReceiver runs on. */ public class SmsReceiverService extends Service { private static final String TAG = "SmsReceiverService"; private ServiceHandler mServiceHandler; private Looper mServiceLooper; public static final String MESSAGE_SENT_ACTION = "com.android.mms.transaction.MESSAGE_SENT"; // This must match the column IDs below. private static final String[] SEND_PROJECTION = new String[] { Sms._ID, //0 Sms.THREAD_ID, //1 Sms.ADDRESS, //2 Sms.BODY, //3 }; public Handler mToastHandler = new Handler() { @Override public void handleMessage(Message msg) { Toast.makeText(SmsReceiverService.this, getString(R.string.message_queued), Toast.LENGTH_SHORT).show(); } }; // This must match SEND_PROJECTION. private static final int SEND_COLUMN_ID = 0; private static final int SEND_COLUMN_THREAD_ID = 1; private static final int SEND_COLUMN_ADDRESS = 2; private static final int SEND_COLUMN_BODY = 3; private int mResultCode; @Override public void onCreate() { if (Log.isLoggable(MmsApp.LOG_TAG, Log.VERBOSE)) { Log.v(TAG, "onCreate"); } // Start up the thread running the service. Note that we create a // separate thread because the service normally runs in the process's // main thread, which we don't want to block. HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND); thread.start(); mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper); } @Override public void onStart(Intent intent, int startId) { if (Log.isLoggable(MmsApp.LOG_TAG, Log.VERBOSE)) { Log.v(TAG, "onStart: #" + startId + ": " + intent.getExtras()); } mResultCode = intent.getIntExtra("result", 0); Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; msg.obj = intent; mServiceHandler.sendMessage(msg); } @Override public void onDestroy() { if (Log.isLoggable(MmsApp.LOG_TAG, Log.VERBOSE)) { Log.v(TAG, "onDestroy"); } mServiceLooper.quit(); } @Override public IBinder onBind(Intent intent) { return null; } private final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { super(looper); } /** * Handle incoming transaction requests. * The incoming requests are initiated by the MMSC Server or by the * MMS Client itself. */ @Override public void handleMessage(Message msg) { if (Log.isLoggable(MmsApp.LOG_TAG, Log.VERBOSE)) { Log.v(TAG, "Handling incoming message: " + msg); } int serviceId = msg.arg1; Intent intent = (Intent)msg.obj; String action = intent.getAction(); if (MESSAGE_SENT_ACTION.equals(intent.getAction())) { handleSmsSent(intent); } else if (SMS_RECEIVED_ACTION.equals(action)) { handleSmsReceived(intent); } else if (ACTION_BOOT_COMPLETED.equals(action)) { handleBootCompleted(); } else if (TelephonyIntents.ACTION_SERVICE_STATE_CHANGED.equals(action)) { handleServiceStateChanged(intent); } // NOTE: We MUST not call stopSelf() directly, since we need to // make sure the wake lock acquired by AlertReceiver is released. SmsReceiver.finishStartingService(SmsReceiverService.this, serviceId); } } private void handleServiceStateChanged(Intent intent) { // If service just returned, start sending out the queued messages ServiceState serviceState = ServiceState.newFromBundle(intent.getExtras()); if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) { sendFirstQueuedMessage(); } } public synchronized void sendFirstQueuedMessage() { // get all the queued messages from the database final Uri uri = Uri.parse("content://sms/queued"); ContentResolver resolver = getContentResolver(); Cursor c = SqliteWrapper.query(this, resolver, uri, SEND_PROJECTION, null, null, null); if (c != null) { try { if (c.moveToFirst()) { int msgId = c.getInt(SEND_COLUMN_ID); String msgText = c.getString(SEND_COLUMN_BODY); String[] address = new String[1]; address[0] = c.getString(SEND_COLUMN_ADDRESS); int threadId = c.getInt(SEND_COLUMN_THREAD_ID); SmsMessageSender sender = new SmsMessageSender(this, address, msgText, threadId); try { sender.sendMessage(SendingProgressTokenManager.NO_TOKEN); // Since sendMessage adds a new message to the outbox rather than // moving the old one, the old one must be deleted here Uri msgUri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgId); SqliteWrapper.delete(this, resolver, msgUri, null, null); } catch (MmsException e) { Log.e(TAG, "Failed to send message: " + e); } } } finally { c.close(); } } } private void handleSmsSent(Intent intent) { Uri uri = intent.getData(); if (mResultCode == Activity.RESULT_OK) { Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_SENT); sendFirstQueuedMessage(); // Update the notification for failed messages since they // may be deleted. MessagingNotification.updateSendFailedNotification( this); } else if ((mResultCode == SmsManager.RESULT_ERROR_RADIO_OFF) || (mResultCode == SmsManager.RESULT_ERROR_NO_SERVICE)) { Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_QUEUED); mToastHandler.sendEmptyMessage(1); } else { Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_FAILED); MessagingNotification.notifySendFailed(getApplicationContext(), true); sendFirstQueuedMessage(); } } private void handleSmsReceived(Intent intent) { SmsMessage[] msgs = Intents.getMessagesFromIntent(intent); Uri messageUri = insertMessage(this, msgs); if (messageUri != null) { MessagingNotification.updateNewMessageIndicator(this, true); } } private void handleBootCompleted() { moveOutboxMessagesToQueuedBox(); sendFirstQueuedMessage(); MessagingNotification.updateNewMessageIndicator(this); } private void moveOutboxMessagesToQueuedBox() { ContentValues values = new ContentValues(1); values.put(Sms.TYPE, Sms.MESSAGE_TYPE_QUEUED); SqliteWrapper.update( getApplicationContext(), getContentResolver(), Outbox.CONTENT_URI, values, "type = " + Sms.MESSAGE_TYPE_OUTBOX, null); } public static final String CLASS_ZERO_BODY_KEY = "CLASS_ZERO_BODY"; // This must match the column IDs below. private final static String[] REPLACE_PROJECTION = new String[] { Sms._ID, Sms.ADDRESS, Sms.PROTOCOL }; // This must match REPLACE_PROJECTION. private static final int REPLACE_COLUMN_ID = 0; /** * If the message is a class-zero message, display it immediately * and return null. Otherwise, store it using the * ContentResolver and return the * Uri of the thread containing this message * so that we can use it for notification. */ private Uri insertMessage(Context context, SmsMessage[] msgs) { // Build the helper classes to parse the messages. SmsMessage sms = msgs[0]; if (sms.getMessageClass() == SmsMessage.MessageClass.CLASS_0) { displayClassZeroMessage(context, sms); return null; } else if (sms.isReplace()) { return replaceMessage(context, msgs); } else { return storeMessage(context, msgs); } } /** * This method is used if this is a "replace short message" SMS. * We find any existing message that matches the incoming * message's originating address and protocol identifier. If * there is one, we replace its fields with those of the new * message. Otherwise, we store the new message as usual. * * See TS 23.040 9.2.3.9. */ private Uri replaceMessage(Context context, SmsMessage[] msgs) { SmsMessage sms = msgs[0]; ContentValues values = extractContentValues(sms); values.put(Inbox.BODY, sms.getMessageBody()); ContentResolver resolver = context.getContentResolver(); String originatingAddress = sms.getOriginatingAddress(); int protocolIdentifier = sms.getProtocolIdentifier(); String selection = Sms.ADDRESS + " = ? AND " + Sms.PROTOCOL + " = ?"; String[] selectionArgs = new String[] { originatingAddress, Integer.toString(protocolIdentifier) }; Cursor cursor = SqliteWrapper.query(context, resolver, Inbox.CONTENT_URI, REPLACE_PROJECTION, selection, selectionArgs, null); if (cursor != null) { try { if (cursor.moveToFirst()) { long messageId = cursor.getLong(REPLACE_COLUMN_ID); Uri messageUri = ContentUris.withAppendedId( Sms.CONTENT_URI, messageId); SqliteWrapper.update(context, resolver, messageUri, values, null, null); return messageUri; } } finally { cursor.close(); } } return storeMessage(context, msgs); } private Uri storeMessage(Context context, SmsMessage[] msgs) { SmsMessage sms = msgs[0]; // Store the message in the content provider. ContentValues values = extractContentValues(sms); int pduCount = msgs.length; if (pduCount == 1) { // There is only one part, so grab the body directly. values.put(Inbox.BODY, sms.getDisplayMessageBody()); } else { // Build up the body from the parts. StringBuilder body = new StringBuilder(); for (int i = 0; i < pduCount; i++) { sms = msgs[i]; body.append(sms.getDisplayMessageBody()); } values.put(Inbox.BODY, body.toString()); } ContentResolver resolver = context.getContentResolver(); return SqliteWrapper.insert(context, resolver, Inbox.CONTENT_URI, values); } /** * Extract all the content values except the body from an SMS * message. */ private ContentValues extractContentValues(SmsMessage sms) { // Store the message in the content provider. ContentValues values = new ContentValues(); values.put(Inbox.ADDRESS, sms.getDisplayOriginatingAddress()); // Use now for the timestamp to avoid confusion with clock // drift between the handset and the SMSC. values.put(Inbox.DATE, new Long(System.currentTimeMillis())); values.put(Inbox.PROTOCOL, sms.getProtocolIdentifier()); values.put(Inbox.READ, Integer.valueOf(0)); if (sms.getPseudoSubject().length() > 0) { values.put(Inbox.SUBJECT, sms.getPseudoSubject()); } values.put(Inbox.REPLY_PATH_PRESENT, sms.isReplyPathPresent() ? 1 : 0); values.put(Inbox.SERVICE_CENTER, sms.getServiceCenterAddress()); return values; } /** * Displays a class-zero message immediately in a pop-up window * with the number from where it received the Notification with * the body of the message * */ private void displayClassZeroMessage(Context context, SmsMessage sms) { // Using NEW_TASK here is necessary because we're calling // startActivity from outside an activity. Intent smsDialogIntent = new Intent(context, ClassZeroActivity.class) .putExtra(CLASS_ZERO_BODY_KEY, sms.getMessageBody()) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); context.startActivity(smsDialogIntent); } }