SmsReceiverService.java revision baf7fec7d1a5b8d52ae7be04865f9e869742c261
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.MmsApp;
26import com.android.mms.ui.ClassZeroActivity;
27import com.android.mms.util.Recycler;
28import com.android.mms.util.SendingProgressTokenManager;
29import com.google.android.mms.MmsException;
30import com.google.android.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.database.Cursor;
40import android.net.Uri;
41import android.os.Handler;
42import android.os.HandlerThread;
43import android.os.IBinder;
44import android.os.Looper;
45import android.os.Message;
46import android.os.Process;
47import android.provider.Telephony.Sms;
48import android.provider.Telephony.Threads;
49import android.provider.Telephony.Sms.Inbox;
50import android.provider.Telephony.Sms.Intents;
51import android.provider.Telephony.Sms.Outbox;
52import android.telephony.ServiceState;
53import android.telephony.SmsManager;
54import android.telephony.SmsMessage;
55import android.util.Log;
56import android.widget.Toast;
57
58import com.android.internal.telephony.TelephonyIntents;
59import com.android.mms.R;
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        if (Log.isLoggable(MmsApp.LOG_TAG, Log.VERBOSE)) {
103            Log.v(TAG, "onCreate");
104        }
105
106        // Start up the thread running the service.  Note that we create a
107        // separate thread because the service normally runs in the process's
108        // main thread, which we don't want to block.
109        HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
110        thread.start();
111
112        mServiceLooper = thread.getLooper();
113        mServiceHandler = new ServiceHandler(mServiceLooper);
114    }
115
116    @Override
117    public void onStart(Intent intent, int startId) {
118        if (Log.isLoggable(MmsApp.LOG_TAG, Log.VERBOSE)) {
119            Log.v(TAG, "onStart: #" + startId + ": " + intent.getExtras());
120        }
121
122        mResultCode = intent.getIntExtra("result", 0);
123
124        Message msg = mServiceHandler.obtainMessage();
125        msg.arg1 = startId;
126        msg.obj = intent;
127        mServiceHandler.sendMessage(msg);
128    }
129
130    @Override
131    public void onDestroy() {
132        if (Log.isLoggable(MmsApp.LOG_TAG, Log.VERBOSE)) {
133            Log.v(TAG, "onDestroy");
134        }
135        mServiceLooper.quit();
136    }
137
138    @Override
139    public IBinder onBind(Intent intent) {
140        return null;
141    }
142
143    private final class ServiceHandler extends Handler {
144        public ServiceHandler(Looper looper) {
145            super(looper);
146        }
147
148        /**
149         * Handle incoming transaction requests.
150         * The incoming requests are initiated by the MMSC Server or by the
151         * MMS Client itself.
152         */
153        @Override
154        public void handleMessage(Message msg) {
155            if (Log.isLoggable(MmsApp.LOG_TAG, Log.VERBOSE)) {
156                Log.v(TAG, "Handling incoming message: " + msg);
157            }
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                    try {
206                        sender.sendMessage(SendingProgressTokenManager.NO_TOKEN);
207
208                        // Since sendMessage adds a new message to the outbox rather than
209                        // moving the old one, the old one must be deleted here
210                        Uri msgUri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgId);
211                        SqliteWrapper.delete(this, resolver, msgUri, null, null);
212                    } catch (MmsException e) {
213                        Log.e(TAG, "Failed to send message: " + e);
214                    }
215                }
216            } finally {
217                c.close();
218            }
219        }
220    }
221
222    private void handleSmsSent(Intent intent) {
223        Uri uri = intent.getData();
224
225        if (mResultCode == Activity.RESULT_OK) {
226            Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_SENT);
227            sendFirstQueuedMessage();
228
229            // Update the notification for failed messages since they
230            // may be deleted.
231            MessagingNotification.updateSendFailedNotification(
232                    this);
233        } else if ((mResultCode == SmsManager.RESULT_ERROR_RADIO_OFF) ||
234                (mResultCode == SmsManager.RESULT_ERROR_NO_SERVICE)) {
235            Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_QUEUED);
236            mToastHandler.sendEmptyMessage(1);
237        } else {
238            Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_FAILED);
239            MessagingNotification.notifySendFailed(getApplicationContext(), true);
240            sendFirstQueuedMessage();
241        }
242    }
243
244    private void handleSmsReceived(Intent intent) {
245        SmsMessage[] msgs = Intents.getMessagesFromIntent(intent);
246        Uri messageUri = insertMessage(this, msgs);
247
248        if (messageUri != null) {
249            MessagingNotification.updateNewMessageIndicator(this, true);
250        }
251    }
252
253    private void handleBootCompleted() {
254        moveOutboxMessagesToQueuedBox();
255        sendFirstQueuedMessage();
256        MessagingNotification.updateNewMessageIndicator(this);
257    }
258
259    private void moveOutboxMessagesToQueuedBox() {
260        ContentValues values = new ContentValues(1);
261
262        values.put(Sms.TYPE, Sms.MESSAGE_TYPE_QUEUED);
263
264        SqliteWrapper.update(
265                getApplicationContext(), getContentResolver(), Outbox.CONTENT_URI,
266                values, "type = " + Sms.MESSAGE_TYPE_OUTBOX, null);
267    }
268
269    public static final String CLASS_ZERO_BODY_KEY = "CLASS_ZERO_BODY";
270
271    // This must match the column IDs below.
272    private final static String[] REPLACE_PROJECTION = new String[] {
273        Sms._ID,
274        Sms.ADDRESS,
275        Sms.PROTOCOL
276    };
277
278    // This must match REPLACE_PROJECTION.
279    private static final int REPLACE_COLUMN_ID = 0;
280
281    /**
282     * If the message is a class-zero message, display it immediately
283     * and return null.  Otherwise, store it using the
284     * <code>ContentResolver</code> and return the
285     * <code>Uri</code> of the thread containing this message
286     * so that we can use it for notification.
287     */
288    private Uri insertMessage(Context context, SmsMessage[] msgs) {
289        // Build the helper classes to parse the messages.
290        SmsMessage sms = msgs[0];
291
292        if (sms.getMessageClass() == SmsMessage.MessageClass.CLASS_0) {
293            displayClassZeroMessage(context, sms);
294            return null;
295        } else if (sms.isReplace()) {
296            return replaceMessage(context, msgs);
297        } else {
298            return storeMessage(context, msgs);
299        }
300    }
301
302    /**
303     * This method is used if this is a "replace short message" SMS.
304     * We find any existing message that matches the incoming
305     * message's originating address and protocol identifier.  If
306     * there is one, we replace its fields with those of the new
307     * message.  Otherwise, we store the new message as usual.
308     *
309     * See TS 23.040 9.2.3.9.
310     */
311    private Uri replaceMessage(Context context, SmsMessage[] msgs) {
312        SmsMessage sms = msgs[0];
313        ContentValues values = extractContentValues(sms);
314
315        values.put(Inbox.BODY, sms.getMessageBody());
316
317        ContentResolver resolver = context.getContentResolver();
318        String originatingAddress = sms.getOriginatingAddress();
319        int protocolIdentifier = sms.getProtocolIdentifier();
320        String selection =
321                Sms.ADDRESS + " = ? AND " +
322                Sms.PROTOCOL + " = ?";
323        String[] selectionArgs = new String[] {
324            originatingAddress, Integer.toString(protocolIdentifier)
325        };
326
327        Cursor cursor = SqliteWrapper.query(context, resolver, Inbox.CONTENT_URI,
328                            REPLACE_PROJECTION, selection, selectionArgs, null);
329
330        if (cursor != null) {
331            try {
332                if (cursor.moveToFirst()) {
333                    long messageId = cursor.getLong(REPLACE_COLUMN_ID);
334                    Uri messageUri = ContentUris.withAppendedId(
335                            Sms.CONTENT_URI, messageId);
336
337                    SqliteWrapper.update(context, resolver, messageUri,
338                                        values, null, null);
339                    return messageUri;
340                }
341            } finally {
342                cursor.close();
343            }
344        }
345        return storeMessage(context, msgs);
346    }
347
348    private Uri storeMessage(Context context, SmsMessage[] msgs) {
349        SmsMessage sms = msgs[0];
350
351        // Store the message in the content provider.
352        ContentValues values = extractContentValues(sms);
353        int pduCount = msgs.length;
354
355        if (pduCount == 1) {
356            // There is only one part, so grab the body directly.
357            values.put(Inbox.BODY, sms.getDisplayMessageBody());
358        } else {
359            // Build up the body from the parts.
360            StringBuilder body = new StringBuilder();
361            for (int i = 0; i < pduCount; i++) {
362                sms = msgs[i];
363                body.append(sms.getDisplayMessageBody());
364            }
365            values.put(Inbox.BODY, body.toString());
366        }
367
368        // Make sure we've got a thread id so after the insert we'll be able to delete
369        // excess messages.
370        Long threadId = values.getAsLong(Sms.THREAD_ID);
371        String address = values.getAsString(Sms.ADDRESS);
372        Contact cacheContact = Contact.get(address,true);
373        if (cacheContact != null) {
374            address = cacheContact.getNumber();
375        }
376
377        if (((threadId == null) || (threadId == 0)) && (address != null)) {
378            values.put(Sms.THREAD_ID, Threads.getOrCreateThreadId(
379                               context, address));
380        }
381
382        ContentResolver resolver = context.getContentResolver();
383
384        Uri insertedUri = SqliteWrapper.insert(context, resolver, Inbox.CONTENT_URI, values);
385
386        // Now make sure we're not over the limit in stored messages
387        threadId = values.getAsLong(Sms.THREAD_ID);
388        Recycler.getSmsRecycler().deleteOldMessagesByThreadId(getApplicationContext(),
389                threadId);
390
391        return insertedUri;
392    }
393
394    /**
395     * Extract all the content values except the body from an SMS
396     * message.
397     */
398    private ContentValues extractContentValues(SmsMessage sms) {
399        // Store the message in the content provider.
400        ContentValues values = new ContentValues();
401
402        values.put(Inbox.ADDRESS, sms.getDisplayOriginatingAddress());
403
404        // Use now for the timestamp to avoid confusion with clock
405        // drift between the handset and the SMSC.
406        values.put(Inbox.DATE, new Long(System.currentTimeMillis()));
407        values.put(Inbox.PROTOCOL, sms.getProtocolIdentifier());
408        values.put(Inbox.READ, Integer.valueOf(0));
409        if (sms.getPseudoSubject().length() > 0) {
410            values.put(Inbox.SUBJECT, sms.getPseudoSubject());
411        }
412        values.put(Inbox.REPLY_PATH_PRESENT, sms.isReplyPathPresent() ? 1 : 0);
413        values.put(Inbox.SERVICE_CENTER, sms.getServiceCenterAddress());
414        return values;
415    }
416
417    /**
418     * Displays a class-zero message immediately in a pop-up window
419     * with the number from where it received the Notification with
420     * the body of the message
421     *
422     */
423    private void displayClassZeroMessage(Context context, SmsMessage sms) {
424        // Using NEW_TASK here is necessary because we're calling
425        // startActivity from outside an activity.
426        Intent smsDialogIntent = new Intent(context, ClassZeroActivity.class)
427                .putExtra("pdu", sms.getPdu())
428                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
429                          | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
430
431        context.startActivity(smsDialogIntent);
432    }
433
434}
435
436
437