Home | History | Annotate | Download | only in transaction
      1 /*
      2  * Copyright (C) 2007-2008 Esmertec AG.
      3  * Copyright (C) 2007-2008 The Android Open Source Project
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.mms.transaction;
     19 
     20 import static android.content.Intent.ACTION_BOOT_COMPLETED;
     21 import static android.provider.Telephony.Sms.Intents.SMS_RECEIVED_ACTION;
     22 
     23 import com.android.mms.data.Contact;
     24 import com.android.mms.ui.ClassZeroActivity;
     25 import com.android.mms.util.Recycler;
     26 import com.android.mms.util.SendingProgressTokenManager;
     27 import com.google.android.mms.MmsException;
     28 import android.database.sqlite.SqliteWrapper;
     29 
     30 import android.app.Activity;
     31 import android.app.Service;
     32 import android.content.ContentResolver;
     33 import android.content.ContentUris;
     34 import android.content.ContentValues;
     35 import android.content.Context;
     36 import android.content.Intent;
     37 import android.content.IntentFilter;
     38 import android.database.Cursor;
     39 import android.net.Uri;
     40 import android.os.Handler;
     41 import android.os.HandlerThread;
     42 import android.os.IBinder;
     43 import android.os.Looper;
     44 import android.os.Message;
     45 import android.os.Process;
     46 import android.provider.Telephony.Sms;
     47 import android.provider.Telephony.Threads;
     48 import android.provider.Telephony.Sms.Inbox;
     49 import android.provider.Telephony.Sms.Intents;
     50 import android.provider.Telephony.Sms.Outbox;
     51 import android.provider.Telephony;
     52 import android.telephony.ServiceState;
     53 import android.telephony.SmsManager;
     54 import android.telephony.SmsMessage;
     55 import android.text.TextUtils;
     56 import android.util.Log;
     57 import android.widget.Toast;
     58 
     59 import com.android.internal.telephony.TelephonyIntents;
     60 import com.android.mms.R;
     61 import com.android.mms.LogTag;
     62 
     63 /**
     64  * This service essentially plays the role of a "worker thread", allowing us to store
     65  * incoming messages to the database, update notifications, etc. without blocking the
     66  * main thread that SmsReceiver runs on.
     67  */
     68 public class SmsReceiverService extends Service {
     69     private static final String TAG = "SmsReceiverService";
     70 
     71     private ServiceHandler mServiceHandler;
     72     private Looper mServiceLooper;
     73     private boolean mSending;
     74 
     75     public static final String MESSAGE_SENT_ACTION =
     76         "com.android.mms.transaction.MESSAGE_SENT";
     77 
     78     // Indicates next message can be picked up and sent out.
     79     public static final String EXTRA_MESSAGE_SENT_SEND_NEXT ="SendNextMsg";
     80 
     81     public static final String ACTION_SEND_MESSAGE =
     82         "com.android.mms.transaction.SEND_MESSAGE";
     83 
     84     // This must match the column IDs below.
     85     private static final String[] SEND_PROJECTION = new String[] {
     86         Sms._ID,        //0
     87         Sms.THREAD_ID,  //1
     88         Sms.ADDRESS,    //2
     89         Sms.BODY,       //3
     90         Sms.STATUS,     //4
     91 
     92     };
     93 
     94     public Handler mToastHandler = new Handler();
     95 
     96     // This must match SEND_PROJECTION.
     97     private static final int SEND_COLUMN_ID         = 0;
     98     private static final int SEND_COLUMN_THREAD_ID  = 1;
     99     private static final int SEND_COLUMN_ADDRESS    = 2;
    100     private static final int SEND_COLUMN_BODY       = 3;
    101     private static final int SEND_COLUMN_STATUS     = 4;
    102 
    103     private int mResultCode;
    104 
    105     @Override
    106     public void onCreate() {
    107         // Temporarily removed for this duplicate message track down.
    108 //        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
    109 //            Log.v(TAG, "onCreate");
    110 //        }
    111 
    112         // Start up the thread running the service.  Note that we create a
    113         // separate thread because the service normally runs in the process's
    114         // main thread, which we don't want to block.
    115         HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
    116         thread.start();
    117 
    118         mServiceLooper = thread.getLooper();
    119         mServiceHandler = new ServiceHandler(mServiceLooper);
    120     }
    121 
    122     @Override
    123     public int onStartCommand(Intent intent, int flags, int startId) {
    124         // Temporarily removed for this duplicate message track down.
    125 //        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
    126 //            Log.v(TAG, "onStart: #" + startId + ": " + intent.getExtras());
    127 //        }
    128 
    129         mResultCode = intent != null ? intent.getIntExtra("result", 0) : 0;
    130 
    131         Message msg = mServiceHandler.obtainMessage();
    132         msg.arg1 = startId;
    133         msg.obj = intent;
    134         mServiceHandler.sendMessage(msg);
    135         return Service.START_NOT_STICKY;
    136     }
    137 
    138     @Override
    139     public void onDestroy() {
    140         // Temporarily removed for this duplicate message track down.
    141 //        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
    142 //            Log.v(TAG, "onDestroy");
    143 //        }
    144         mServiceLooper.quit();
    145     }
    146 
    147     @Override
    148     public IBinder onBind(Intent intent) {
    149         return null;
    150     }
    151 
    152     private final class ServiceHandler extends Handler {
    153         public ServiceHandler(Looper looper) {
    154             super(looper);
    155         }
    156 
    157         /**
    158          * Handle incoming transaction requests.
    159          * The incoming requests are initiated by the MMSC Server or by the MMS Client itself.
    160          */
    161         @Override
    162         public void handleMessage(Message msg) {
    163             int serviceId = msg.arg1;
    164             Intent intent = (Intent)msg.obj;
    165             if (intent != null) {
    166                 String action = intent.getAction();
    167 
    168                 int error = intent.getIntExtra("errorCode", 0);
    169 
    170                 if (MESSAGE_SENT_ACTION.equals(intent.getAction())) {
    171                     handleSmsSent(intent, error);
    172                 } else if (SMS_RECEIVED_ACTION.equals(action)) {
    173                     handleSmsReceived(intent, error);
    174                 } else if (ACTION_BOOT_COMPLETED.equals(action)) {
    175                     handleBootCompleted();
    176                 } else if (TelephonyIntents.ACTION_SERVICE_STATE_CHANGED.equals(action)) {
    177                     handleServiceStateChanged(intent);
    178                 } else if (ACTION_SEND_MESSAGE.endsWith(action)) {
    179                     handleSendMessage();
    180                 }
    181             }
    182             // NOTE: We MUST not call stopSelf() directly, since we need to
    183             // make sure the wake lock acquired by AlertReceiver is released.
    184             SmsReceiver.finishStartingService(SmsReceiverService.this, serviceId);
    185         }
    186     }
    187 
    188     private void handleServiceStateChanged(Intent intent) {
    189         // If service just returned, start sending out the queued messages
    190         ServiceState serviceState = ServiceState.newFromBundle(intent.getExtras());
    191         if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
    192             sendFirstQueuedMessage();
    193         }
    194     }
    195 
    196     private void handleSendMessage() {
    197         if (!mSending) {
    198             sendFirstQueuedMessage();
    199         }
    200     }
    201 
    202     public synchronized void sendFirstQueuedMessage() {
    203         boolean success = true;
    204         // get all the queued messages from the database
    205         final Uri uri = Uri.parse("content://sms/queued");
    206         ContentResolver resolver = getContentResolver();
    207         Cursor c = SqliteWrapper.query(this, resolver, uri,
    208                         SEND_PROJECTION, null, null, "date ASC");   // date ASC so we send out in
    209                                                                     // same order the user tried
    210                                                                     // to send messages.
    211         if (c != null) {
    212             try {
    213                 if (c.moveToFirst()) {
    214                     String msgText = c.getString(SEND_COLUMN_BODY);
    215                     String address = c.getString(SEND_COLUMN_ADDRESS);
    216                     int threadId = c.getInt(SEND_COLUMN_THREAD_ID);
    217                     int status = c.getInt(SEND_COLUMN_STATUS);
    218 
    219                     int msgId = c.getInt(SEND_COLUMN_ID);
    220                     Uri msgUri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgId);
    221 
    222                     SmsMessageSender sender = new SmsSingleRecipientSender(this,
    223                             address, msgText, threadId, status == Sms.STATUS_PENDING,
    224                             msgUri);
    225 
    226                     if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
    227                         Log.v(TAG, "sendFirstQueuedMessage " + msgUri +
    228                                 ", address: " + address +
    229                                 ", threadId: " + threadId +
    230                                 ", body: " + msgText);
    231                     }
    232                     try {
    233                         sender.sendMessage(SendingProgressTokenManager.NO_TOKEN);;
    234                         mSending = true;
    235                     } catch (MmsException e) {
    236                         Log.e(TAG, "sendFirstQueuedMessage: failed to send message " + msgUri
    237                                 + ", caught ", e);
    238                         mSending = false;
    239                         messageFailedToSend(msgUri, SmsManager.RESULT_ERROR_GENERIC_FAILURE);
    240                         success = false;
    241                     }
    242                 }
    243             } finally {
    244                 c.close();
    245             }
    246         }
    247         if (success) {
    248             // We successfully sent all the messages in the queue. We don't need to
    249             // be notified of any service changes any longer.
    250             unRegisterForServiceStateChanges();
    251         }
    252     }
    253 
    254     private void handleSmsSent(Intent intent, int error) {
    255         Uri uri = intent.getData();
    256         mSending = false;
    257         boolean sendNextMsg = intent.getBooleanExtra(EXTRA_MESSAGE_SENT_SEND_NEXT, false);
    258 
    259         if (mResultCode == Activity.RESULT_OK) {
    260             if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
    261                 Log.v(TAG, "handleSmsSent sending uri: " + uri);
    262             }
    263             if (!Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_SENT, error)) {
    264                 Log.e(TAG, "handleSmsSent: failed to move message " + uri + " to sent folder");
    265             }
    266             if (sendNextMsg) {
    267                 sendFirstQueuedMessage();
    268             }
    269 
    270             // Update the notification for failed messages since they may be deleted.
    271             MessagingNotification.updateSendFailedNotification(this);
    272         } else if ((mResultCode == SmsManager.RESULT_ERROR_RADIO_OFF) ||
    273                 (mResultCode == SmsManager.RESULT_ERROR_NO_SERVICE)) {
    274             if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
    275                 Log.v(TAG, "handleSmsSent: no service, queuing message w/ uri: " + uri);
    276             }
    277             // We got an error with no service or no radio. Register for state changes so
    278             // when the status of the connection/radio changes, we can try to send the
    279             // queued up messages.
    280             registerForServiceStateChanges();
    281             // We couldn't send the message, put in the queue to retry later.
    282             Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_QUEUED, error);
    283             mToastHandler.post(new Runnable() {
    284                 public void run() {
    285                     Toast.makeText(SmsReceiverService.this, getString(R.string.message_queued),
    286                             Toast.LENGTH_SHORT).show();
    287                 }
    288             });
    289         } else if (mResultCode == SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE) {
    290             mToastHandler.post(new Runnable() {
    291                 public void run() {
    292                     Toast.makeText(SmsReceiverService.this, getString(R.string.fdn_check_failure),
    293                             Toast.LENGTH_SHORT).show();
    294                 }
    295             });
    296         } else {
    297             messageFailedToSend(uri, error);
    298             if (sendNextMsg) {
    299                 sendFirstQueuedMessage();
    300             }
    301         }
    302     }
    303 
    304     private void messageFailedToSend(Uri uri, int error) {
    305         if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
    306             Log.v(TAG, "messageFailedToSend msg failed uri: " + uri);
    307         }
    308         Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_FAILED, error);
    309         MessagingNotification.notifySendFailed(getApplicationContext(), true);
    310     }
    311 
    312     private void handleSmsReceived(Intent intent, int error) {
    313         SmsMessage[] msgs = Intents.getMessagesFromIntent(intent);
    314         Uri messageUri = insertMessage(this, msgs, error);
    315 
    316         if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
    317             SmsMessage sms = msgs[0];
    318             Log.v(TAG, "handleSmsReceived" + (sms.isReplace() ? "(replace)" : "") +
    319                     " messageUri: " + messageUri +
    320                     ", address: " + sms.getOriginatingAddress() +
    321                     ", body: " + sms.getMessageBody());
    322         }
    323 
    324         if (messageUri != null) {
    325             // Called off of the UI thread so ok to block.
    326             MessagingNotification.blockingUpdateNewMessageIndicator(this, true, false);
    327         }
    328     }
    329 
    330     private void handleBootCompleted() {
    331         moveOutboxMessagesToQueuedBox();
    332         sendFirstQueuedMessage();
    333 
    334         // Called off of the UI thread so ok to block.
    335         MessagingNotification.blockingUpdateNewMessageIndicator(this, true, false);
    336     }
    337 
    338     private void moveOutboxMessagesToQueuedBox() {
    339         ContentValues values = new ContentValues(1);
    340 
    341         values.put(Sms.TYPE, Sms.MESSAGE_TYPE_QUEUED);
    342 
    343         SqliteWrapper.update(
    344                 getApplicationContext(), getContentResolver(), Outbox.CONTENT_URI,
    345                 values, "type = " + Sms.MESSAGE_TYPE_OUTBOX, null);
    346     }
    347 
    348     public static final String CLASS_ZERO_BODY_KEY = "CLASS_ZERO_BODY";
    349 
    350     // This must match the column IDs below.
    351     private final static String[] REPLACE_PROJECTION = new String[] {
    352         Sms._ID,
    353         Sms.ADDRESS,
    354         Sms.PROTOCOL
    355     };
    356 
    357     // This must match REPLACE_PROJECTION.
    358     private static final int REPLACE_COLUMN_ID = 0;
    359 
    360     /**
    361      * If the message is a class-zero message, display it immediately
    362      * and return null.  Otherwise, store it using the
    363      * <code>ContentResolver</code> and return the
    364      * <code>Uri</code> of the thread containing this message
    365      * so that we can use it for notification.
    366      */
    367     private Uri insertMessage(Context context, SmsMessage[] msgs, int error) {
    368         // Build the helper classes to parse the messages.
    369         SmsMessage sms = msgs[0];
    370 
    371         if (sms.getMessageClass() == SmsMessage.MessageClass.CLASS_0) {
    372             displayClassZeroMessage(context, sms);
    373             return null;
    374         } else if (sms.isReplace()) {
    375             return replaceMessage(context, msgs, error);
    376         } else {
    377             return storeMessage(context, msgs, error);
    378         }
    379     }
    380 
    381     /**
    382      * This method is used if this is a "replace short message" SMS.
    383      * We find any existing message that matches the incoming
    384      * message's originating address and protocol identifier.  If
    385      * there is one, we replace its fields with those of the new
    386      * message.  Otherwise, we store the new message as usual.
    387      *
    388      * See TS 23.040 9.2.3.9.
    389      */
    390     private Uri replaceMessage(Context context, SmsMessage[] msgs, int error) {
    391         SmsMessage sms = msgs[0];
    392         ContentValues values = extractContentValues(sms);
    393 
    394         values.put(Inbox.BODY, sms.getMessageBody());
    395         values.put(Sms.ERROR_CODE, error);
    396 
    397         ContentResolver resolver = context.getContentResolver();
    398         String originatingAddress = sms.getOriginatingAddress();
    399         int protocolIdentifier = sms.getProtocolIdentifier();
    400         String selection =
    401                 Sms.ADDRESS + " = ? AND " +
    402                 Sms.PROTOCOL + " = ?";
    403         String[] selectionArgs = new String[] {
    404             originatingAddress, Integer.toString(protocolIdentifier)
    405         };
    406 
    407         Cursor cursor = SqliteWrapper.query(context, resolver, Inbox.CONTENT_URI,
    408                             REPLACE_PROJECTION, selection, selectionArgs, null);
    409 
    410         if (cursor != null) {
    411             try {
    412                 if (cursor.moveToFirst()) {
    413                     long messageId = cursor.getLong(REPLACE_COLUMN_ID);
    414                     Uri messageUri = ContentUris.withAppendedId(
    415                             Sms.CONTENT_URI, messageId);
    416 
    417                     SqliteWrapper.update(context, resolver, messageUri,
    418                                         values, null, null);
    419                     return messageUri;
    420                 }
    421             } finally {
    422                 cursor.close();
    423             }
    424         }
    425         return storeMessage(context, msgs, error);
    426     }
    427 
    428     private Uri storeMessage(Context context, SmsMessage[] msgs, int error) {
    429         SmsMessage sms = msgs[0];
    430 
    431         // Store the message in the content provider.
    432         ContentValues values = extractContentValues(sms);
    433         values.put(Sms.ERROR_CODE, error);
    434         int pduCount = msgs.length;
    435 
    436         if (pduCount == 1) {
    437             // There is only one part, so grab the body directly.
    438             values.put(Inbox.BODY, sms.getDisplayMessageBody());
    439         } else {
    440             // Build up the body from the parts.
    441             StringBuilder body = new StringBuilder();
    442             for (int i = 0; i < pduCount; i++) {
    443                 sms = msgs[i];
    444                 body.append(sms.getDisplayMessageBody());
    445             }
    446             values.put(Inbox.BODY, body.toString());
    447         }
    448 
    449         // Make sure we've got a thread id so after the insert we'll be able to delete
    450         // excess messages.
    451         Long threadId = values.getAsLong(Sms.THREAD_ID);
    452         String address = values.getAsString(Sms.ADDRESS);
    453         if (!TextUtils.isEmpty(address)) {
    454             Contact cacheContact = Contact.get(address,true);
    455             if (cacheContact != null) {
    456                 address = cacheContact.getNumber();
    457             }
    458         } else {
    459             address = getString(R.string.unknown_sender);
    460             values.put(Sms.ADDRESS, address);
    461         }
    462 
    463         if (((threadId == null) || (threadId == 0)) && (address != null)) {
    464             threadId = Threads.getOrCreateThreadId(context, address);
    465             values.put(Sms.THREAD_ID, threadId);
    466         }
    467 
    468         ContentResolver resolver = context.getContentResolver();
    469 
    470         Uri insertedUri = SqliteWrapper.insert(context, resolver, Inbox.CONTENT_URI, values);
    471 
    472         // Now make sure we're not over the limit in stored messages
    473         Recycler.getSmsRecycler().deleteOldMessagesByThreadId(getApplicationContext(), threadId);
    474 
    475         return insertedUri;
    476     }
    477 
    478     /**
    479      * Extract all the content values except the body from an SMS
    480      * message.
    481      */
    482     private ContentValues extractContentValues(SmsMessage sms) {
    483         // Store the message in the content provider.
    484         ContentValues values = new ContentValues();
    485 
    486         values.put(Inbox.ADDRESS, sms.getDisplayOriginatingAddress());
    487 
    488         // Use now for the timestamp to avoid confusion with clock
    489         // drift between the handset and the SMSC.
    490         values.put(Inbox.DATE, new Long(System.currentTimeMillis()));
    491         values.put(Inbox.PROTOCOL, sms.getProtocolIdentifier());
    492         values.put(Inbox.READ, 0);
    493         values.put(Inbox.SEEN, 0);
    494         if (sms.getPseudoSubject().length() > 0) {
    495             values.put(Inbox.SUBJECT, sms.getPseudoSubject());
    496         }
    497         values.put(Inbox.REPLY_PATH_PRESENT, sms.isReplyPathPresent() ? 1 : 0);
    498         values.put(Inbox.SERVICE_CENTER, sms.getServiceCenterAddress());
    499         return values;
    500     }
    501 
    502     /**
    503      * Displays a class-zero message immediately in a pop-up window
    504      * with the number from where it received the Notification with
    505      * the body of the message
    506      *
    507      */
    508     private void displayClassZeroMessage(Context context, SmsMessage sms) {
    509         // Using NEW_TASK here is necessary because we're calling
    510         // startActivity from outside an activity.
    511         Intent smsDialogIntent = new Intent(context, ClassZeroActivity.class)
    512                 .putExtra("pdu", sms.getPdu())
    513                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
    514                           | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
    515 
    516         context.startActivity(smsDialogIntent);
    517     }
    518 
    519     private void registerForServiceStateChanges() {
    520         Context context = getApplicationContext();
    521         unRegisterForServiceStateChanges();
    522 
    523         IntentFilter intentFilter = new IntentFilter();
    524         intentFilter.addAction(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED);
    525         if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
    526             Log.v(TAG, "registerForServiceStateChanges");
    527         }
    528 
    529         context.registerReceiver(SmsReceiver.getInstance(), intentFilter);
    530     }
    531 
    532     private void unRegisterForServiceStateChanges() {
    533         if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
    534             Log.v(TAG, "unRegisterForServiceStateChanges");
    535         }
    536         try {
    537             Context context = getApplicationContext();
    538             context.unregisterReceiver(SmsReceiver.getInstance());
    539         } catch (IllegalArgumentException e) {
    540             // Allow un-matched register-unregister calls
    541         }
    542     }
    543 
    544 }
    545 
    546 
    547