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