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