SmsReceiverService.java revision 0d2c0042be90f42635e3bc301f2a2e37460e6344
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 android.provider.Telephony.Sms.Intents.SMS_RECEIVED_ACTION;
22
23import com.android.mms.data.Contact;
24import com.android.mms.ui.ClassZeroActivity;
25import com.android.mms.util.Recycler;
26import com.android.mms.util.SendingProgressTokenManager;
27import com.google.android.mms.MmsException;
28import android.database.sqlite.SqliteWrapper;
29
30import android.app.Activity;
31import android.app.Service;
32import android.content.ContentResolver;
33import android.content.ContentUris;
34import android.content.ContentValues;
35import android.content.Context;
36import android.content.Intent;
37import android.content.IntentFilter;
38import android.database.Cursor;
39import android.net.Uri;
40import android.os.Handler;
41import android.os.HandlerThread;
42import android.os.IBinder;
43import android.os.Looper;
44import android.os.Message;
45import android.os.Process;
46import android.provider.Telephony.Sms;
47import android.provider.Telephony.Threads;
48import android.provider.Telephony.Sms.Inbox;
49import android.provider.Telephony.Sms.Intents;
50import android.provider.Telephony.Sms.Outbox;
51import android.provider.Telephony;
52import android.telephony.ServiceState;
53import android.telephony.SmsManager;
54import android.telephony.SmsMessage;
55import android.text.TextUtils;
56import android.util.Log;
57import android.widget.Toast;
58
59import com.android.internal.telephony.TelephonyIntents;
60import com.android.mms.R;
61import 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 */
68public 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) || LogTag.DEBUG_SEND) {
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) || LogTag.DEBUG_SEND) {
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) || LogTag.DEBUG_SEND) {
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 (LogTag.DEBUG_SEND) {
166                Log.v(TAG, "handleMessage serviceId: " + serviceId + " intent: " + intent);
167            }
168            if (intent != null) {
169                String action = intent.getAction();
170
171                int error = intent.getIntExtra("errorCode", 0);
172
173                if (LogTag.DEBUG_SEND) {
174                    Log.v(TAG, "handleMessage action: " + action + " error: " + error);
175                }
176
177                if (MESSAGE_SENT_ACTION.equals(intent.getAction())) {
178                    handleSmsSent(intent, error);
179                } else if (SMS_RECEIVED_ACTION.equals(action)) {
180                    handleSmsReceived(intent, error);
181                } else if (ACTION_BOOT_COMPLETED.equals(action)) {
182                    handleBootCompleted();
183                } else if (TelephonyIntents.ACTION_SERVICE_STATE_CHANGED.equals(action)) {
184                    handleServiceStateChanged(intent);
185                } else if (ACTION_SEND_MESSAGE.endsWith(action)) {
186                    handleSendMessage();
187                }
188            }
189            // NOTE: We MUST not call stopSelf() directly, since we need to
190            // make sure the wake lock acquired by AlertReceiver is released.
191            SmsReceiver.finishStartingService(SmsReceiverService.this, serviceId);
192        }
193    }
194
195    private void handleServiceStateChanged(Intent intent) {
196        // If service just returned, start sending out the queued messages
197        ServiceState serviceState = ServiceState.newFromBundle(intent.getExtras());
198        if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
199            sendFirstQueuedMessage();
200        }
201    }
202
203    private void handleSendMessage() {
204        if (!mSending) {
205            sendFirstQueuedMessage();
206        }
207    }
208
209    public synchronized void sendFirstQueuedMessage() {
210        boolean success = true;
211        // get all the queued messages from the database
212        final Uri uri = Uri.parse("content://sms/queued");
213        ContentResolver resolver = getContentResolver();
214        Cursor c = SqliteWrapper.query(this, resolver, uri,
215                        SEND_PROJECTION, null, null, "date ASC");   // date ASC so we send out in
216                                                                    // same order the user tried
217                                                                    // to send messages.
218        if (c != null) {
219            try {
220                if (c.moveToFirst()) {
221                    String msgText = c.getString(SEND_COLUMN_BODY);
222                    String address = c.getString(SEND_COLUMN_ADDRESS);
223                    int threadId = c.getInt(SEND_COLUMN_THREAD_ID);
224                    int status = c.getInt(SEND_COLUMN_STATUS);
225
226                    int msgId = c.getInt(SEND_COLUMN_ID);
227                    Uri msgUri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgId);
228
229                    SmsMessageSender sender = new SmsSingleRecipientSender(this,
230                            address, msgText, threadId, status == Sms.STATUS_PENDING,
231                            msgUri);
232
233                    if (LogTag.VERBOSE || Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
234                        Log.v(TAG, "sendFirstQueuedMessage " + msgUri +
235                                ", address: " + address +
236                                ", threadId: " + threadId);
237                    }
238
239                    try {
240                        sender.sendMessage(SendingProgressTokenManager.NO_TOKEN);;
241                        mSending = true;
242                    } catch (MmsException e) {
243                        Log.e(TAG, "sendFirstQueuedMessage: failed to send message " + msgUri
244                                + ", caught ", e);
245                        mSending = false;
246                        messageFailedToSend(msgUri, SmsManager.RESULT_ERROR_GENERIC_FAILURE);
247                        success = false;
248                    }
249                }
250            } finally {
251                c.close();
252            }
253        }
254        if (success) {
255            // We successfully sent all the messages in the queue. We don't need to
256            // be notified of any service changes any longer.
257            unRegisterForServiceStateChanges();
258        }
259    }
260
261    private void handleSmsSent(Intent intent, int error) {
262        Uri uri = intent.getData();
263        mSending = false;
264        boolean sendNextMsg = intent.getBooleanExtra(EXTRA_MESSAGE_SENT_SEND_NEXT, false);
265
266        if (LogTag.DEBUG_SEND) {
267            Log.v(TAG, "handleSmsSent sending uri: " + uri + " sendNextMsg: " + sendNextMsg +
268                    " mResultCode: " + mResultCode + " error: " + error);
269        }
270
271        if (mResultCode == Activity.RESULT_OK) {
272            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
273                Log.v(TAG, "handleSmsSent sending uri: " + uri);
274            }
275            if (!Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_SENT, error)) {
276                Log.e(TAG, "handleSmsSent: failed to move message " + uri + " to sent folder");
277            }
278            if (sendNextMsg) {
279                sendFirstQueuedMessage();
280            }
281
282            // Update the notification for failed messages since they may be deleted.
283            MessagingNotification.updateSendFailedNotification(this);
284        } else if ((mResultCode == SmsManager.RESULT_ERROR_RADIO_OFF) ||
285                (mResultCode == SmsManager.RESULT_ERROR_NO_SERVICE)) {
286            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
287                Log.v(TAG, "handleSmsSent: no service, queuing message w/ uri: " + uri);
288            }
289            // We got an error with no service or no radio. Register for state changes so
290            // when the status of the connection/radio changes, we can try to send the
291            // queued up messages.
292            registerForServiceStateChanges();
293            // We couldn't send the message, put in the queue to retry later.
294            Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_QUEUED, error);
295            mToastHandler.post(new Runnable() {
296                public void run() {
297                    Toast.makeText(SmsReceiverService.this, getString(R.string.message_queued),
298                            Toast.LENGTH_SHORT).show();
299                }
300            });
301        } else if (mResultCode == SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE) {
302            mToastHandler.post(new Runnable() {
303                public void run() {
304                    Toast.makeText(SmsReceiverService.this, getString(R.string.fdn_check_failure),
305                            Toast.LENGTH_SHORT).show();
306                }
307            });
308        } else {
309            messageFailedToSend(uri, error);
310            if (sendNextMsg) {
311                sendFirstQueuedMessage();
312            }
313        }
314    }
315
316    private void messageFailedToSend(Uri uri, int error) {
317        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE) || LogTag.DEBUG_SEND) {
318            Log.v(TAG, "messageFailedToSend msg failed uri: " + uri + " error: " + error);
319        }
320        Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_FAILED, error);
321        MessagingNotification.notifySendFailed(getApplicationContext(), true);
322    }
323
324    private void handleSmsReceived(Intent intent, int error) {
325        SmsMessage[] msgs = Intents.getMessagesFromIntent(intent);
326        Uri messageUri = insertMessage(this, msgs, error);
327
328        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE) || LogTag.DEBUG_SEND) {
329            SmsMessage sms = msgs[0];
330            Log.v(TAG, "handleSmsReceived" + (sms.isReplace() ? "(replace)" : "") +
331                    " messageUri: " + messageUri +
332                    ", address: " + sms.getOriginatingAddress() +
333                    ", body: " + sms.getMessageBody());
334        }
335
336        if (messageUri != null) {
337            // Called off of the UI thread so ok to block.
338            MessagingNotification.blockingUpdateNewMessageIndicator(this, true, false);
339        }
340    }
341
342    private void handleBootCompleted() {
343        moveOutboxMessagesToQueuedBox();
344        sendFirstQueuedMessage();
345
346        // Called off of the UI thread so ok to block.
347        MessagingNotification.blockingUpdateNewMessageIndicator(this, true, false);
348    }
349
350    private void moveOutboxMessagesToQueuedBox() {
351        ContentValues values = new ContentValues(1);
352
353        values.put(Sms.TYPE, Sms.MESSAGE_TYPE_QUEUED);
354
355        SqliteWrapper.update(
356                getApplicationContext(), getContentResolver(), Outbox.CONTENT_URI,
357                values, "type = " + Sms.MESSAGE_TYPE_OUTBOX, null);
358    }
359
360    public static final String CLASS_ZERO_BODY_KEY = "CLASS_ZERO_BODY";
361
362    // This must match the column IDs below.
363    private final static String[] REPLACE_PROJECTION = new String[] {
364        Sms._ID,
365        Sms.ADDRESS,
366        Sms.PROTOCOL
367    };
368
369    // This must match REPLACE_PROJECTION.
370    private static final int REPLACE_COLUMN_ID = 0;
371
372    /**
373     * If the message is a class-zero message, display it immediately
374     * and return null.  Otherwise, store it using the
375     * <code>ContentResolver</code> and return the
376     * <code>Uri</code> of the thread containing this message
377     * so that we can use it for notification.
378     */
379    private Uri insertMessage(Context context, SmsMessage[] msgs, int error) {
380        // Build the helper classes to parse the messages.
381        SmsMessage sms = msgs[0];
382
383        if (sms.getMessageClass() == SmsMessage.MessageClass.CLASS_0) {
384            displayClassZeroMessage(context, sms);
385            return null;
386        } else if (sms.isReplace()) {
387            return replaceMessage(context, msgs, error);
388        } else {
389            return storeMessage(context, msgs, error);
390        }
391    }
392
393    /**
394     * This method is used if this is a "replace short message" SMS.
395     * We find any existing message that matches the incoming
396     * message's originating address and protocol identifier.  If
397     * there is one, we replace its fields with those of the new
398     * message.  Otherwise, we store the new message as usual.
399     *
400     * See TS 23.040 9.2.3.9.
401     */
402    private Uri replaceMessage(Context context, SmsMessage[] msgs, int error) {
403        SmsMessage sms = msgs[0];
404        ContentValues values = extractContentValues(sms);
405
406        values.put(Inbox.BODY, sms.getMessageBody());
407        values.put(Sms.ERROR_CODE, error);
408
409        ContentResolver resolver = context.getContentResolver();
410        String originatingAddress = sms.getOriginatingAddress();
411        int protocolIdentifier = sms.getProtocolIdentifier();
412        String selection =
413                Sms.ADDRESS + " = ? AND " +
414                Sms.PROTOCOL + " = ?";
415        String[] selectionArgs = new String[] {
416            originatingAddress, Integer.toString(protocolIdentifier)
417        };
418
419        Cursor cursor = SqliteWrapper.query(context, resolver, Inbox.CONTENT_URI,
420                            REPLACE_PROJECTION, selection, selectionArgs, null);
421
422        if (cursor != null) {
423            try {
424                if (cursor.moveToFirst()) {
425                    long messageId = cursor.getLong(REPLACE_COLUMN_ID);
426                    Uri messageUri = ContentUris.withAppendedId(
427                            Sms.CONTENT_URI, messageId);
428
429                    SqliteWrapper.update(context, resolver, messageUri,
430                                        values, null, null);
431                    return messageUri;
432                }
433            } finally {
434                cursor.close();
435            }
436        }
437        return storeMessage(context, msgs, error);
438    }
439
440    public static String replaceFormFeeds(String s) {
441        // Some providers send formfeeds in their messages. Convert those formfeeds to newlines.
442        return s.replace('\f', '\n');
443    }
444
445    private Uri storeMessage(Context context, SmsMessage[] msgs, int error) {
446        SmsMessage sms = msgs[0];
447
448        // Store the message in the content provider.
449        ContentValues values = extractContentValues(sms);
450        values.put(Sms.ERROR_CODE, error);
451        int pduCount = msgs.length;
452
453        if (pduCount == 1) {
454            // There is only one part, so grab the body directly.
455            values.put(Inbox.BODY, replaceFormFeeds(sms.getDisplayMessageBody()));
456        } else {
457            // Build up the body from the parts.
458            StringBuilder body = new StringBuilder();
459            for (int i = 0; i < pduCount; i++) {
460                sms = msgs[i];
461                if (sms.mWrappedSmsMessage != null) {
462                    body.append(sms.getDisplayMessageBody());
463                }
464            }
465            values.put(Inbox.BODY, replaceFormFeeds(body.toString()));
466        }
467
468        // Make sure we've got a thread id so after the insert we'll be able to delete
469        // excess messages.
470        Long threadId = values.getAsLong(Sms.THREAD_ID);
471        String address = values.getAsString(Sms.ADDRESS);
472        if (!TextUtils.isEmpty(address)) {
473            Contact cacheContact = Contact.get(address,true);
474            if (cacheContact != null) {
475                address = cacheContact.getNumber();
476            }
477        } else {
478            address = getString(R.string.unknown_sender);
479            values.put(Sms.ADDRESS, address);
480        }
481
482        if (((threadId == null) || (threadId == 0)) && (address != null)) {
483            threadId = Threads.getOrCreateThreadId(context, address);
484            values.put(Sms.THREAD_ID, threadId);
485        }
486
487        ContentResolver resolver = context.getContentResolver();
488
489        Uri insertedUri = SqliteWrapper.insert(context, resolver, Inbox.CONTENT_URI, values);
490
491        // Now make sure we're not over the limit in stored messages
492        Recycler.getSmsRecycler().deleteOldMessagesByThreadId(getApplicationContext(), threadId);
493
494        return insertedUri;
495    }
496
497    /**
498     * Extract all the content values except the body from an SMS
499     * message.
500     */
501    private ContentValues extractContentValues(SmsMessage sms) {
502        // Store the message in the content provider.
503        ContentValues values = new ContentValues();
504
505        values.put(Inbox.ADDRESS, sms.getDisplayOriginatingAddress());
506
507        // Use now for the timestamp to avoid confusion with clock
508        // drift between the handset and the SMSC.
509        values.put(Inbox.DATE, new Long(System.currentTimeMillis()));
510        values.put(Inbox.DATE_SENT, Long.valueOf(sms.getTimestampMillis()));
511        values.put(Inbox.PROTOCOL, sms.getProtocolIdentifier());
512        values.put(Inbox.READ, 0);
513        values.put(Inbox.SEEN, 0);
514        if (sms.getPseudoSubject().length() > 0) {
515            values.put(Inbox.SUBJECT, sms.getPseudoSubject());
516        }
517        values.put(Inbox.REPLY_PATH_PRESENT, sms.isReplyPathPresent() ? 1 : 0);
518        values.put(Inbox.SERVICE_CENTER, sms.getServiceCenterAddress());
519        return values;
520    }
521
522    /**
523     * Displays a class-zero message immediately in a pop-up window
524     * with the number from where it received the Notification with
525     * the body of the message
526     *
527     */
528    private void displayClassZeroMessage(Context context, SmsMessage sms) {
529        // Using NEW_TASK here is necessary because we're calling
530        // startActivity from outside an activity.
531        Intent smsDialogIntent = new Intent(context, ClassZeroActivity.class)
532                .putExtra("pdu", sms.getPdu())
533                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
534                          | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
535
536        context.startActivity(smsDialogIntent);
537    }
538
539    private void registerForServiceStateChanges() {
540        Context context = getApplicationContext();
541        unRegisterForServiceStateChanges();
542
543        IntentFilter intentFilter = new IntentFilter();
544        intentFilter.addAction(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED);
545        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE) || LogTag.DEBUG_SEND) {
546            Log.v(TAG, "registerForServiceStateChanges");
547        }
548
549        context.registerReceiver(SmsReceiver.getInstance(), intentFilter);
550    }
551
552    private void unRegisterForServiceStateChanges() {
553        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE) || LogTag.DEBUG_SEND) {
554            Log.v(TAG, "unRegisterForServiceStateChanges");
555        }
556        try {
557            Context context = getApplicationContext();
558            context.unregisterReceiver(SmsReceiver.getInstance());
559        } catch (IllegalArgumentException e) {
560            // Allow un-matched register-unregister calls
561        }
562    }
563
564}
565
566
567