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