SmsReceiverService.java revision f3d4a23a76a4bbe0d48503ad7abf5d8f4fc38c31
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
154         * MMS Client itself.
155         */
156        @Override
157        public void handleMessage(Message msg) {
158            int serviceId = msg.arg1;
159            Intent intent = (Intent)msg.obj;
160
161            String action = intent.getAction();
162
163            if (MESSAGE_SENT_ACTION.equals(intent.getAction())) {
164                handleSmsSent(intent);
165            } else if (SMS_RECEIVED_ACTION.equals(action)) {
166                handleSmsReceived(intent);
167            } else if (ACTION_BOOT_COMPLETED.equals(action)) {
168                handleBootCompleted();
169            } else if (TelephonyIntents.ACTION_SERVICE_STATE_CHANGED.equals(action)) {
170                handleServiceStateChanged(intent);
171            }
172
173            // NOTE: We MUST not call stopSelf() directly, since we need to
174            // make sure the wake lock acquired by AlertReceiver is released.
175            SmsReceiver.finishStartingService(SmsReceiverService.this, serviceId);
176        }
177    }
178
179    private void handleServiceStateChanged(Intent intent) {
180        // If service just returned, start sending out the queued messages
181        ServiceState serviceState = ServiceState.newFromBundle(intent.getExtras());
182        if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
183            sendFirstQueuedMessage();
184        }
185    }
186
187    public synchronized void sendFirstQueuedMessage() {
188        // get all the queued messages from the database
189        final Uri uri = Uri.parse("content://sms/queued");
190        ContentResolver resolver = getContentResolver();
191        Cursor c = SqliteWrapper.query(this, resolver, uri,
192                        SEND_PROJECTION, null, null, null);
193
194        if (c != null) {
195            try {
196                if (c.moveToFirst()) {
197                    int msgId = c.getInt(SEND_COLUMN_ID);
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                    try {
207                        sender.sendMessage(SendingProgressTokenManager.NO_TOKEN);
208
209                        // Since sendMessage adds a new message to the outbox rather than
210                        // moving the old one, the old one must be deleted here
211                        Uri msgUri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgId);
212                        SqliteWrapper.delete(this, resolver, msgUri, null, null);
213
214                        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
215                            Log.v(TAG, "sendFirstQueuedMessage after send msgUri: " + msgUri +
216                                    " address " + address +
217                                    " threadId: " + threadId +
218                                    " body: " + msgText);
219                        }
220                    } catch (MmsException e) {
221                        Log.e(TAG, "Failed to send message: " + e);
222                    }
223                }
224            } finally {
225                c.close();
226            }
227        }
228    }
229
230    private void handleSmsSent(Intent intent) {
231        Uri uri = intent.getData();
232
233        if (mResultCode == Activity.RESULT_OK) {
234            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
235                Log.v(TAG, "handleSmsSent sending uri: " + uri);
236            }
237            Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_SENT);
238            sendFirstQueuedMessage();
239
240            // Update the notification for failed messages since they
241            // may be deleted.
242            MessagingNotification.updateSendFailedNotification(
243                    this);
244        } else if ((mResultCode == SmsManager.RESULT_ERROR_RADIO_OFF) ||
245                (mResultCode == SmsManager.RESULT_ERROR_NO_SERVICE)) {
246            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
247                Log.v(TAG, "handleSmsSent: no service, queuing message w/ uri: " + uri);
248            }
249            Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_QUEUED);
250            mToastHandler.sendEmptyMessage(1);
251        } else {
252            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
253                Log.v(TAG, "handleSmsSent msg failed uri: " + uri);
254            }
255            Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_FAILED);
256            MessagingNotification.notifySendFailed(getApplicationContext(), true);
257            sendFirstQueuedMessage();
258        }
259    }
260
261    private void handleSmsReceived(Intent intent) {
262        SmsMessage[] msgs = Intents.getMessagesFromIntent(intent);
263        Uri messageUri = insertMessage(this, msgs);
264
265        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
266            if (msgs.length > 0 && msgs[0] != null) {
267                Log.v(TAG, "handleSmsReceived messageUri: " + messageUri + " address: " +
268                        msgs[0].getOriginatingAddress() +
269                        " body: " + msgs[0].getMessageBody());
270            } else {
271                Log.v(TAG, "handleSmsReceived messageUri: " + messageUri + " but null msg");
272            }
273        }
274
275        if (messageUri != null) {
276            MessagingNotification.updateNewMessageIndicator(this, true);
277        }
278    }
279
280    private void handleBootCompleted() {
281        moveOutboxMessagesToQueuedBox();
282        sendFirstQueuedMessage();
283        MessagingNotification.updateNewMessageIndicator(this);
284    }
285
286    private void moveOutboxMessagesToQueuedBox() {
287        ContentValues values = new ContentValues(1);
288
289        values.put(Sms.TYPE, Sms.MESSAGE_TYPE_QUEUED);
290
291        SqliteWrapper.update(
292                getApplicationContext(), getContentResolver(), Outbox.CONTENT_URI,
293                values, "type = " + Sms.MESSAGE_TYPE_OUTBOX, null);
294    }
295
296    public static final String CLASS_ZERO_BODY_KEY = "CLASS_ZERO_BODY";
297
298    // This must match the column IDs below.
299    private final static String[] REPLACE_PROJECTION = new String[] {
300        Sms._ID,
301        Sms.ADDRESS,
302        Sms.PROTOCOL
303    };
304
305    // This must match REPLACE_PROJECTION.
306    private static final int REPLACE_COLUMN_ID = 0;
307
308    /**
309     * If the message is a class-zero message, display it immediately
310     * and return null.  Otherwise, store it using the
311     * <code>ContentResolver</code> and return the
312     * <code>Uri</code> of the thread containing this message
313     * so that we can use it for notification.
314     */
315    private Uri insertMessage(Context context, SmsMessage[] msgs) {
316        // Build the helper classes to parse the messages.
317        SmsMessage sms = msgs[0];
318
319        if (sms.getMessageClass() == SmsMessage.MessageClass.CLASS_0) {
320            displayClassZeroMessage(context, sms);
321            return null;
322        } else if (sms.isReplace()) {
323            return replaceMessage(context, msgs);
324        } else {
325            return storeMessage(context, msgs);
326        }
327    }
328
329    /**
330     * This method is used if this is a "replace short message" SMS.
331     * We find any existing message that matches the incoming
332     * message's originating address and protocol identifier.  If
333     * there is one, we replace its fields with those of the new
334     * message.  Otherwise, we store the new message as usual.
335     *
336     * See TS 23.040 9.2.3.9.
337     */
338    private Uri replaceMessage(Context context, SmsMessage[] msgs) {
339        SmsMessage sms = msgs[0];
340        ContentValues values = extractContentValues(sms);
341
342        values.put(Inbox.BODY, sms.getMessageBody());
343
344        ContentResolver resolver = context.getContentResolver();
345        String originatingAddress = sms.getOriginatingAddress();
346        int protocolIdentifier = sms.getProtocolIdentifier();
347        String selection =
348                Sms.ADDRESS + " = ? AND " +
349                Sms.PROTOCOL + " = ?";
350        String[] selectionArgs = new String[] {
351            originatingAddress, Integer.toString(protocolIdentifier)
352        };
353
354        Cursor cursor = SqliteWrapper.query(context, resolver, Inbox.CONTENT_URI,
355                            REPLACE_PROJECTION, selection, selectionArgs, null);
356
357        if (cursor != null) {
358            try {
359                if (cursor.moveToFirst()) {
360                    long messageId = cursor.getLong(REPLACE_COLUMN_ID);
361                    Uri messageUri = ContentUris.withAppendedId(
362                            Sms.CONTENT_URI, messageId);
363
364                    SqliteWrapper.update(context, resolver, messageUri,
365                                        values, null, null);
366                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
367                        Log.v(TAG, "replaceMessage after update messageUri: " + messageUri +
368                                " address: " + sms.getOriginatingAddress() +
369                                " body: " + sms.getMessageBody());
370                    }
371                    return messageUri;
372                }
373            } finally {
374                cursor.close();
375            }
376        }
377        return storeMessage(context, msgs);
378    }
379
380    private Uri storeMessage(Context context, SmsMessage[] msgs) {
381        SmsMessage sms = msgs[0];
382
383        // Store the message in the content provider.
384        ContentValues values = extractContentValues(sms);
385        int pduCount = msgs.length;
386
387        if (pduCount == 1) {
388            // There is only one part, so grab the body directly.
389            values.put(Inbox.BODY, sms.getDisplayMessageBody());
390        } else {
391            // Build up the body from the parts.
392            StringBuilder body = new StringBuilder();
393            for (int i = 0; i < pduCount; i++) {
394                sms = msgs[i];
395                body.append(sms.getDisplayMessageBody());
396            }
397            values.put(Inbox.BODY, body.toString());
398        }
399
400        // Make sure we've got a thread id so after the insert we'll be able to delete
401        // excess messages.
402        Long threadId = values.getAsLong(Sms.THREAD_ID);
403        String address = values.getAsString(Sms.ADDRESS);
404        Contact cacheContact = Contact.get(address,true);
405        if (cacheContact != null) {
406            address = cacheContact.getNumber();
407        }
408
409        if (((threadId == null) || (threadId == 0)) && (address != null)) {
410            values.put(Sms.THREAD_ID, Threads.getOrCreateThreadId(
411                               context, address));
412        }
413
414        ContentResolver resolver = context.getContentResolver();
415
416        Uri insertedUri = SqliteWrapper.insert(context, resolver, Inbox.CONTENT_URI, values);
417
418        // Now make sure we're not over the limit in stored messages
419        threadId = values.getAsLong(Sms.THREAD_ID);
420        Recycler.getSmsRecycler().deleteOldMessagesByThreadId(getApplicationContext(),
421                threadId);
422
423        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
424            Log.v(TAG, "storeMessage insertedUri: " + insertedUri +
425                    " address: " + sms.getOriginatingAddress() +
426                    " body: " + sms.getMessageBody());
427        }
428        return insertedUri;
429    }
430
431    /**
432     * Extract all the content values except the body from an SMS
433     * message.
434     */
435    private ContentValues extractContentValues(SmsMessage sms) {
436        // Store the message in the content provider.
437        ContentValues values = new ContentValues();
438
439        values.put(Inbox.ADDRESS, sms.getDisplayOriginatingAddress());
440
441        // Use now for the timestamp to avoid confusion with clock
442        // drift between the handset and the SMSC.
443        values.put(Inbox.DATE, new Long(System.currentTimeMillis()));
444        values.put(Inbox.PROTOCOL, sms.getProtocolIdentifier());
445        values.put(Inbox.READ, Integer.valueOf(0));
446        if (sms.getPseudoSubject().length() > 0) {
447            values.put(Inbox.SUBJECT, sms.getPseudoSubject());
448        }
449        values.put(Inbox.REPLY_PATH_PRESENT, sms.isReplyPathPresent() ? 1 : 0);
450        values.put(Inbox.SERVICE_CENTER, sms.getServiceCenterAddress());
451        return values;
452    }
453
454    /**
455     * Displays a class-zero message immediately in a pop-up window
456     * with the number from where it received the Notification with
457     * the body of the message
458     *
459     */
460    private void displayClassZeroMessage(Context context, SmsMessage sms) {
461        // Using NEW_TASK here is necessary because we're calling
462        // startActivity from outside an activity.
463        Intent smsDialogIntent = new Intent(context, ClassZeroActivity.class)
464                .putExtra("pdu", sms.getPdu())
465                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
466                          | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
467
468        context.startActivity(smsDialogIntent);
469    }
470
471}
472
473
474