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