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