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