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