SmsReceiverService.java revision 0d2c0042be90f42635e3bc301f2a2e37460e6344
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.mms.data.Contact; 24import com.android.mms.ui.ClassZeroActivity; 25import com.android.mms.util.Recycler; 26import com.android.mms.util.SendingProgressTokenManager; 27import com.google.android.mms.MmsException; 28import android.database.sqlite.SqliteWrapper; 29 30import android.app.Activity; 31import android.app.Service; 32import android.content.ContentResolver; 33import android.content.ContentUris; 34import android.content.ContentValues; 35import android.content.Context; 36import android.content.Intent; 37import android.content.IntentFilter; 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.provider.Telephony; 52import android.telephony.ServiceState; 53import android.telephony.SmsManager; 54import android.telephony.SmsMessage; 55import android.text.TextUtils; 56import android.util.Log; 57import android.widget.Toast; 58 59import com.android.internal.telephony.TelephonyIntents; 60import com.android.mms.R; 61import com.android.mms.LogTag; 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 */ 68public class SmsReceiverService extends Service { 69 private static final String TAG = "SmsReceiverService"; 70 71 private ServiceHandler mServiceHandler; 72 private Looper mServiceLooper; 73 private boolean mSending; 74 75 public static final String MESSAGE_SENT_ACTION = 76 "com.android.mms.transaction.MESSAGE_SENT"; 77 78 // Indicates next message can be picked up and sent out. 79 public static final String EXTRA_MESSAGE_SENT_SEND_NEXT ="SendNextMsg"; 80 81 public static final String ACTION_SEND_MESSAGE = 82 "com.android.mms.transaction.SEND_MESSAGE"; 83 84 // This must match the column IDs below. 85 private static final String[] SEND_PROJECTION = new String[] { 86 Sms._ID, //0 87 Sms.THREAD_ID, //1 88 Sms.ADDRESS, //2 89 Sms.BODY, //3 90 Sms.STATUS, //4 91 92 }; 93 94 public Handler mToastHandler = new Handler(); 95 96 // This must match SEND_PROJECTION. 97 private static final int SEND_COLUMN_ID = 0; 98 private static final int SEND_COLUMN_THREAD_ID = 1; 99 private static final int SEND_COLUMN_ADDRESS = 2; 100 private static final int SEND_COLUMN_BODY = 3; 101 private static final int SEND_COLUMN_STATUS = 4; 102 103 private int mResultCode; 104 105 @Override 106 public void onCreate() { 107 // Temporarily removed for this duplicate message track down. 108// if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE) || LogTag.DEBUG_SEND) { 109// Log.v(TAG, "onCreate"); 110// } 111 112 // Start up the thread running the service. Note that we create a 113 // separate thread because the service normally runs in the process's 114 // main thread, which we don't want to block. 115 HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND); 116 thread.start(); 117 118 mServiceLooper = thread.getLooper(); 119 mServiceHandler = new ServiceHandler(mServiceLooper); 120 } 121 122 @Override 123 public int onStartCommand(Intent intent, int flags, int startId) { 124 // Temporarily removed for this duplicate message track down. 125// if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE) || LogTag.DEBUG_SEND) { 126// Log.v(TAG, "onStart: #" + startId + ": " + intent.getExtras()); 127// } 128 129 mResultCode = intent != null ? intent.getIntExtra("result", 0) : 0; 130 131 Message msg = mServiceHandler.obtainMessage(); 132 msg.arg1 = startId; 133 msg.obj = intent; 134 mServiceHandler.sendMessage(msg); 135 return Service.START_NOT_STICKY; 136 } 137 138 @Override 139 public void onDestroy() { 140 // Temporarily removed for this duplicate message track down. 141// if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE) || LogTag.DEBUG_SEND) { 142// Log.v(TAG, "onDestroy"); 143// } 144 mServiceLooper.quit(); 145 } 146 147 @Override 148 public IBinder onBind(Intent intent) { 149 return null; 150 } 151 152 private final class ServiceHandler extends Handler { 153 public ServiceHandler(Looper looper) { 154 super(looper); 155 } 156 157 /** 158 * Handle incoming transaction requests. 159 * The incoming requests are initiated by the MMSC Server or by the MMS Client itself. 160 */ 161 @Override 162 public void handleMessage(Message msg) { 163 int serviceId = msg.arg1; 164 Intent intent = (Intent)msg.obj; 165 if (LogTag.DEBUG_SEND) { 166 Log.v(TAG, "handleMessage serviceId: " + serviceId + " intent: " + intent); 167 } 168 if (intent != null) { 169 String action = intent.getAction(); 170 171 int error = intent.getIntExtra("errorCode", 0); 172 173 if (LogTag.DEBUG_SEND) { 174 Log.v(TAG, "handleMessage action: " + action + " error: " + error); 175 } 176 177 if (MESSAGE_SENT_ACTION.equals(intent.getAction())) { 178 handleSmsSent(intent, error); 179 } else if (SMS_RECEIVED_ACTION.equals(action)) { 180 handleSmsReceived(intent, error); 181 } else if (ACTION_BOOT_COMPLETED.equals(action)) { 182 handleBootCompleted(); 183 } else if (TelephonyIntents.ACTION_SERVICE_STATE_CHANGED.equals(action)) { 184 handleServiceStateChanged(intent); 185 } else if (ACTION_SEND_MESSAGE.endsWith(action)) { 186 handleSendMessage(); 187 } 188 } 189 // NOTE: We MUST not call stopSelf() directly, since we need to 190 // make sure the wake lock acquired by AlertReceiver is released. 191 SmsReceiver.finishStartingService(SmsReceiverService.this, serviceId); 192 } 193 } 194 195 private void handleServiceStateChanged(Intent intent) { 196 // If service just returned, start sending out the queued messages 197 ServiceState serviceState = ServiceState.newFromBundle(intent.getExtras()); 198 if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) { 199 sendFirstQueuedMessage(); 200 } 201 } 202 203 private void handleSendMessage() { 204 if (!mSending) { 205 sendFirstQueuedMessage(); 206 } 207 } 208 209 public synchronized void sendFirstQueuedMessage() { 210 boolean success = true; 211 // get all the queued messages from the database 212 final Uri uri = Uri.parse("content://sms/queued"); 213 ContentResolver resolver = getContentResolver(); 214 Cursor c = SqliteWrapper.query(this, resolver, uri, 215 SEND_PROJECTION, null, null, "date ASC"); // date ASC so we send out in 216 // same order the user tried 217 // to send messages. 218 if (c != null) { 219 try { 220 if (c.moveToFirst()) { 221 String msgText = c.getString(SEND_COLUMN_BODY); 222 String address = c.getString(SEND_COLUMN_ADDRESS); 223 int threadId = c.getInt(SEND_COLUMN_THREAD_ID); 224 int status = c.getInt(SEND_COLUMN_STATUS); 225 226 int msgId = c.getInt(SEND_COLUMN_ID); 227 Uri msgUri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgId); 228 229 SmsMessageSender sender = new SmsSingleRecipientSender(this, 230 address, msgText, threadId, status == Sms.STATUS_PENDING, 231 msgUri); 232 233 if (LogTag.VERBOSE || Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) { 234 Log.v(TAG, "sendFirstQueuedMessage " + msgUri + 235 ", address: " + address + 236 ", threadId: " + threadId); 237 } 238 239 try { 240 sender.sendMessage(SendingProgressTokenManager.NO_TOKEN);; 241 mSending = true; 242 } catch (MmsException e) { 243 Log.e(TAG, "sendFirstQueuedMessage: failed to send message " + msgUri 244 + ", caught ", e); 245 mSending = false; 246 messageFailedToSend(msgUri, SmsManager.RESULT_ERROR_GENERIC_FAILURE); 247 success = false; 248 } 249 } 250 } finally { 251 c.close(); 252 } 253 } 254 if (success) { 255 // We successfully sent all the messages in the queue. We don't need to 256 // be notified of any service changes any longer. 257 unRegisterForServiceStateChanges(); 258 } 259 } 260 261 private void handleSmsSent(Intent intent, int error) { 262 Uri uri = intent.getData(); 263 mSending = false; 264 boolean sendNextMsg = intent.getBooleanExtra(EXTRA_MESSAGE_SENT_SEND_NEXT, false); 265 266 if (LogTag.DEBUG_SEND) { 267 Log.v(TAG, "handleSmsSent sending uri: " + uri + " sendNextMsg: " + sendNextMsg + 268 " mResultCode: " + mResultCode + " error: " + error); 269 } 270 271 if (mResultCode == Activity.RESULT_OK) { 272 if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) { 273 Log.v(TAG, "handleSmsSent sending uri: " + uri); 274 } 275 if (!Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_SENT, error)) { 276 Log.e(TAG, "handleSmsSent: failed to move message " + uri + " to sent folder"); 277 } 278 if (sendNextMsg) { 279 sendFirstQueuedMessage(); 280 } 281 282 // Update the notification for failed messages since they may be deleted. 283 MessagingNotification.updateSendFailedNotification(this); 284 } else if ((mResultCode == SmsManager.RESULT_ERROR_RADIO_OFF) || 285 (mResultCode == SmsManager.RESULT_ERROR_NO_SERVICE)) { 286 if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) { 287 Log.v(TAG, "handleSmsSent: no service, queuing message w/ uri: " + uri); 288 } 289 // We got an error with no service or no radio. Register for state changes so 290 // when the status of the connection/radio changes, we can try to send the 291 // queued up messages. 292 registerForServiceStateChanges(); 293 // We couldn't send the message, put in the queue to retry later. 294 Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_QUEUED, error); 295 mToastHandler.post(new Runnable() { 296 public void run() { 297 Toast.makeText(SmsReceiverService.this, getString(R.string.message_queued), 298 Toast.LENGTH_SHORT).show(); 299 } 300 }); 301 } else if (mResultCode == SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE) { 302 mToastHandler.post(new Runnable() { 303 public void run() { 304 Toast.makeText(SmsReceiverService.this, getString(R.string.fdn_check_failure), 305 Toast.LENGTH_SHORT).show(); 306 } 307 }); 308 } else { 309 messageFailedToSend(uri, error); 310 if (sendNextMsg) { 311 sendFirstQueuedMessage(); 312 } 313 } 314 } 315 316 private void messageFailedToSend(Uri uri, int error) { 317 if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE) || LogTag.DEBUG_SEND) { 318 Log.v(TAG, "messageFailedToSend msg failed uri: " + uri + " error: " + error); 319 } 320 Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_FAILED, error); 321 MessagingNotification.notifySendFailed(getApplicationContext(), true); 322 } 323 324 private void handleSmsReceived(Intent intent, int error) { 325 SmsMessage[] msgs = Intents.getMessagesFromIntent(intent); 326 Uri messageUri = insertMessage(this, msgs, error); 327 328 if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE) || LogTag.DEBUG_SEND) { 329 SmsMessage sms = msgs[0]; 330 Log.v(TAG, "handleSmsReceived" + (sms.isReplace() ? "(replace)" : "") + 331 " messageUri: " + messageUri + 332 ", address: " + sms.getOriginatingAddress() + 333 ", body: " + sms.getMessageBody()); 334 } 335 336 if (messageUri != null) { 337 // Called off of the UI thread so ok to block. 338 MessagingNotification.blockingUpdateNewMessageIndicator(this, true, false); 339 } 340 } 341 342 private void handleBootCompleted() { 343 moveOutboxMessagesToQueuedBox(); 344 sendFirstQueuedMessage(); 345 346 // Called off of the UI thread so ok to block. 347 MessagingNotification.blockingUpdateNewMessageIndicator(this, true, false); 348 } 349 350 private void moveOutboxMessagesToQueuedBox() { 351 ContentValues values = new ContentValues(1); 352 353 values.put(Sms.TYPE, Sms.MESSAGE_TYPE_QUEUED); 354 355 SqliteWrapper.update( 356 getApplicationContext(), getContentResolver(), Outbox.CONTENT_URI, 357 values, "type = " + Sms.MESSAGE_TYPE_OUTBOX, null); 358 } 359 360 public static final String CLASS_ZERO_BODY_KEY = "CLASS_ZERO_BODY"; 361 362 // This must match the column IDs below. 363 private final static String[] REPLACE_PROJECTION = new String[] { 364 Sms._ID, 365 Sms.ADDRESS, 366 Sms.PROTOCOL 367 }; 368 369 // This must match REPLACE_PROJECTION. 370 private static final int REPLACE_COLUMN_ID = 0; 371 372 /** 373 * If the message is a class-zero message, display it immediately 374 * and return null. Otherwise, store it using the 375 * <code>ContentResolver</code> and return the 376 * <code>Uri</code> of the thread containing this message 377 * so that we can use it for notification. 378 */ 379 private Uri insertMessage(Context context, SmsMessage[] msgs, int error) { 380 // Build the helper classes to parse the messages. 381 SmsMessage sms = msgs[0]; 382 383 if (sms.getMessageClass() == SmsMessage.MessageClass.CLASS_0) { 384 displayClassZeroMessage(context, sms); 385 return null; 386 } else if (sms.isReplace()) { 387 return replaceMessage(context, msgs, error); 388 } else { 389 return storeMessage(context, msgs, error); 390 } 391 } 392 393 /** 394 * This method is used if this is a "replace short message" SMS. 395 * We find any existing message that matches the incoming 396 * message's originating address and protocol identifier. If 397 * there is one, we replace its fields with those of the new 398 * message. Otherwise, we store the new message as usual. 399 * 400 * See TS 23.040 9.2.3.9. 401 */ 402 private Uri replaceMessage(Context context, SmsMessage[] msgs, int error) { 403 SmsMessage sms = msgs[0]; 404 ContentValues values = extractContentValues(sms); 405 406 values.put(Inbox.BODY, sms.getMessageBody()); 407 values.put(Sms.ERROR_CODE, error); 408 409 ContentResolver resolver = context.getContentResolver(); 410 String originatingAddress = sms.getOriginatingAddress(); 411 int protocolIdentifier = sms.getProtocolIdentifier(); 412 String selection = 413 Sms.ADDRESS + " = ? AND " + 414 Sms.PROTOCOL + " = ?"; 415 String[] selectionArgs = new String[] { 416 originatingAddress, Integer.toString(protocolIdentifier) 417 }; 418 419 Cursor cursor = SqliteWrapper.query(context, resolver, Inbox.CONTENT_URI, 420 REPLACE_PROJECTION, selection, selectionArgs, null); 421 422 if (cursor != null) { 423 try { 424 if (cursor.moveToFirst()) { 425 long messageId = cursor.getLong(REPLACE_COLUMN_ID); 426 Uri messageUri = ContentUris.withAppendedId( 427 Sms.CONTENT_URI, messageId); 428 429 SqliteWrapper.update(context, resolver, messageUri, 430 values, null, null); 431 return messageUri; 432 } 433 } finally { 434 cursor.close(); 435 } 436 } 437 return storeMessage(context, msgs, error); 438 } 439 440 public static String replaceFormFeeds(String s) { 441 // Some providers send formfeeds in their messages. Convert those formfeeds to newlines. 442 return s.replace('\f', '\n'); 443 } 444 445 private Uri storeMessage(Context context, SmsMessage[] msgs, int error) { 446 SmsMessage sms = msgs[0]; 447 448 // Store the message in the content provider. 449 ContentValues values = extractContentValues(sms); 450 values.put(Sms.ERROR_CODE, error); 451 int pduCount = msgs.length; 452 453 if (pduCount == 1) { 454 // There is only one part, so grab the body directly. 455 values.put(Inbox.BODY, replaceFormFeeds(sms.getDisplayMessageBody())); 456 } else { 457 // Build up the body from the parts. 458 StringBuilder body = new StringBuilder(); 459 for (int i = 0; i < pduCount; i++) { 460 sms = msgs[i]; 461 if (sms.mWrappedSmsMessage != null) { 462 body.append(sms.getDisplayMessageBody()); 463 } 464 } 465 values.put(Inbox.BODY, replaceFormFeeds(body.toString())); 466 } 467 468 // Make sure we've got a thread id so after the insert we'll be able to delete 469 // excess messages. 470 Long threadId = values.getAsLong(Sms.THREAD_ID); 471 String address = values.getAsString(Sms.ADDRESS); 472 if (!TextUtils.isEmpty(address)) { 473 Contact cacheContact = Contact.get(address,true); 474 if (cacheContact != null) { 475 address = cacheContact.getNumber(); 476 } 477 } else { 478 address = getString(R.string.unknown_sender); 479 values.put(Sms.ADDRESS, address); 480 } 481 482 if (((threadId == null) || (threadId == 0)) && (address != null)) { 483 threadId = Threads.getOrCreateThreadId(context, address); 484 values.put(Sms.THREAD_ID, threadId); 485 } 486 487 ContentResolver resolver = context.getContentResolver(); 488 489 Uri insertedUri = SqliteWrapper.insert(context, resolver, Inbox.CONTENT_URI, values); 490 491 // Now make sure we're not over the limit in stored messages 492 Recycler.getSmsRecycler().deleteOldMessagesByThreadId(getApplicationContext(), threadId); 493 494 return insertedUri; 495 } 496 497 /** 498 * Extract all the content values except the body from an SMS 499 * message. 500 */ 501 private ContentValues extractContentValues(SmsMessage sms) { 502 // Store the message in the content provider. 503 ContentValues values = new ContentValues(); 504 505 values.put(Inbox.ADDRESS, sms.getDisplayOriginatingAddress()); 506 507 // Use now for the timestamp to avoid confusion with clock 508 // drift between the handset and the SMSC. 509 values.put(Inbox.DATE, new Long(System.currentTimeMillis())); 510 values.put(Inbox.DATE_SENT, Long.valueOf(sms.getTimestampMillis())); 511 values.put(Inbox.PROTOCOL, sms.getProtocolIdentifier()); 512 values.put(Inbox.READ, 0); 513 values.put(Inbox.SEEN, 0); 514 if (sms.getPseudoSubject().length() > 0) { 515 values.put(Inbox.SUBJECT, sms.getPseudoSubject()); 516 } 517 values.put(Inbox.REPLY_PATH_PRESENT, sms.isReplyPathPresent() ? 1 : 0); 518 values.put(Inbox.SERVICE_CENTER, sms.getServiceCenterAddress()); 519 return values; 520 } 521 522 /** 523 * Displays a class-zero message immediately in a pop-up window 524 * with the number from where it received the Notification with 525 * the body of the message 526 * 527 */ 528 private void displayClassZeroMessage(Context context, SmsMessage sms) { 529 // Using NEW_TASK here is necessary because we're calling 530 // startActivity from outside an activity. 531 Intent smsDialogIntent = new Intent(context, ClassZeroActivity.class) 532 .putExtra("pdu", sms.getPdu()) 533 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 534 | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 535 536 context.startActivity(smsDialogIntent); 537 } 538 539 private void registerForServiceStateChanges() { 540 Context context = getApplicationContext(); 541 unRegisterForServiceStateChanges(); 542 543 IntentFilter intentFilter = new IntentFilter(); 544 intentFilter.addAction(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED); 545 if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE) || LogTag.DEBUG_SEND) { 546 Log.v(TAG, "registerForServiceStateChanges"); 547 } 548 549 context.registerReceiver(SmsReceiver.getInstance(), intentFilter); 550 } 551 552 private void unRegisterForServiceStateChanges() { 553 if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE) || LogTag.DEBUG_SEND) { 554 Log.v(TAG, "unRegisterForServiceStateChanges"); 555 } 556 try { 557 Context context = getApplicationContext(); 558 context.unregisterReceiver(SmsReceiver.getInstance()); 559 } catch (IllegalArgumentException e) { 560 // Allow un-matched register-unregister calls 561 } 562 } 563 564} 565 566 567