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