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