/*
* Copyright (C) 2007-2008 Esmertec AG.
* Copyright (C) 2007-2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mms.transaction;
import static android.content.Intent.ACTION_BOOT_COMPLETED;
import static android.provider.Telephony.Sms.Intents.SMS_RECEIVED_ACTION;
import com.android.mms.MmsApp;
import com.android.mms.ui.ClassZeroActivity;
import com.android.mms.util.SendingProgressTokenManager;
import com.google.android.mms.MmsException;
import com.google.android.mms.util.SqliteWrapper;
import android.app.Activity;
import android.app.Service;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.provider.Telephony.Sms;
import android.provider.Telephony.Sms.Inbox;
import android.provider.Telephony.Sms.Intents;
import android.provider.Telephony.Sms.Outbox;
import android.telephony.ServiceState;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.util.Log;
import android.widget.Toast;
import com.android.internal.telephony.TelephonyIntents;
import com.android.mms.R;
import com.android.mms.ui.ClassZeroActivity;
import com.android.mms.util.SendingProgressTokenManager;
import com.google.android.mms.MmsException;
import com.google.android.mms.util.SqliteWrapper;
/**
* This service essentially plays the role of a "worker thread", allowing us to store
* incoming messages to the database, update notifications, etc. without blocking the
* main thread that SmsReceiver runs on.
*/
public class SmsReceiverService extends Service {
private static final String TAG = "SmsReceiverService";
private ServiceHandler mServiceHandler;
private Looper mServiceLooper;
public static final String MESSAGE_SENT_ACTION =
"com.android.mms.transaction.MESSAGE_SENT";
// This must match the column IDs below.
private static final String[] SEND_PROJECTION = new String[] {
Sms._ID, //0
Sms.THREAD_ID, //1
Sms.ADDRESS, //2
Sms.BODY, //3
};
public Handler mToastHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Toast.makeText(SmsReceiverService.this, getString(R.string.message_queued),
Toast.LENGTH_SHORT).show();
}
};
// This must match SEND_PROJECTION.
private static final int SEND_COLUMN_ID = 0;
private static final int SEND_COLUMN_THREAD_ID = 1;
private static final int SEND_COLUMN_ADDRESS = 2;
private static final int SEND_COLUMN_BODY = 3;
private int mResultCode;
@Override
public void onCreate() {
if (Log.isLoggable(MmsApp.LOG_TAG, Log.VERBOSE)) {
Log.v(TAG, "onCreate");
}
// Start up the thread running the service. Note that we create a
// separate thread because the service normally runs in the process's
// main thread, which we don't want to block.
HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
@Override
public void onStart(Intent intent, int startId) {
if (Log.isLoggable(MmsApp.LOG_TAG, Log.VERBOSE)) {
Log.v(TAG, "onStart: #" + startId + ": " + intent.getExtras());
}
mResultCode = intent.getIntExtra("result", 0);
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}
@Override
public void onDestroy() {
if (Log.isLoggable(MmsApp.LOG_TAG, Log.VERBOSE)) {
Log.v(TAG, "onDestroy");
}
mServiceLooper.quit();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
/**
* Handle incoming transaction requests.
* The incoming requests are initiated by the MMSC Server or by the
* MMS Client itself.
*/
@Override
public void handleMessage(Message msg) {
if (Log.isLoggable(MmsApp.LOG_TAG, Log.VERBOSE)) {
Log.v(TAG, "Handling incoming message: " + msg);
}
int serviceId = msg.arg1;
Intent intent = (Intent)msg.obj;
String action = intent.getAction();
if (MESSAGE_SENT_ACTION.equals(intent.getAction())) {
handleSmsSent(intent);
} else if (SMS_RECEIVED_ACTION.equals(action)) {
handleSmsReceived(intent);
} else if (ACTION_BOOT_COMPLETED.equals(action)) {
handleBootCompleted();
} else if (TelephonyIntents.ACTION_SERVICE_STATE_CHANGED.equals(action)) {
handleServiceStateChanged(intent);
}
// NOTE: We MUST not call stopSelf() directly, since we need to
// make sure the wake lock acquired by AlertReceiver is released.
SmsReceiver.finishStartingService(SmsReceiverService.this, serviceId);
}
}
private void handleServiceStateChanged(Intent intent) {
// If service just returned, start sending out the queued messages
ServiceState serviceState = ServiceState.newFromBundle(intent.getExtras());
if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
sendFirstQueuedMessage();
}
}
public synchronized void sendFirstQueuedMessage() {
// get all the queued messages from the database
final Uri uri = Uri.parse("content://sms/queued");
ContentResolver resolver = getContentResolver();
Cursor c = SqliteWrapper.query(this, resolver, uri,
SEND_PROJECTION, null, null, null);
if (c != null) {
try {
if (c.moveToFirst()) {
int msgId = c.getInt(SEND_COLUMN_ID);
String msgText = c.getString(SEND_COLUMN_BODY);
String[] address = new String[1];
address[0] = c.getString(SEND_COLUMN_ADDRESS);
int threadId = c.getInt(SEND_COLUMN_THREAD_ID);
SmsMessageSender sender = new SmsMessageSender(this,
address, msgText, threadId);
try {
sender.sendMessage(SendingProgressTokenManager.NO_TOKEN);
// Since sendMessage adds a new message to the outbox rather than
// moving the old one, the old one must be deleted here
Uri msgUri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgId);
SqliteWrapper.delete(this, resolver, msgUri, null, null);
} catch (MmsException e) {
Log.e(TAG, "Failed to send message: " + e);
}
}
} finally {
c.close();
}
}
}
private void handleSmsSent(Intent intent) {
Uri uri = intent.getData();
if (mResultCode == Activity.RESULT_OK) {
Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_SENT);
sendFirstQueuedMessage();
// Update the notification for failed messages since they
// may be deleted.
MessagingNotification.updateSendFailedNotification(
this);
} else if ((mResultCode == SmsManager.RESULT_ERROR_RADIO_OFF) ||
(mResultCode == SmsManager.RESULT_ERROR_NO_SERVICE)) {
Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_QUEUED);
mToastHandler.sendEmptyMessage(1);
} else {
Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_FAILED);
MessagingNotification.notifySendFailed(getApplicationContext(), true);
sendFirstQueuedMessage();
}
}
private void handleSmsReceived(Intent intent) {
SmsMessage[] msgs = Intents.getMessagesFromIntent(intent);
Uri messageUri = insertMessage(this, msgs);
if (messageUri != null) {
MessagingNotification.updateNewMessageIndicator(this, true);
}
}
private void handleBootCompleted() {
moveOutboxMessagesToQueuedBox();
sendFirstQueuedMessage();
MessagingNotification.updateNewMessageIndicator(this);
}
private void moveOutboxMessagesToQueuedBox() {
ContentValues values = new ContentValues(1);
values.put(Sms.TYPE, Sms.MESSAGE_TYPE_QUEUED);
SqliteWrapper.update(
getApplicationContext(), getContentResolver(), Outbox.CONTENT_URI,
values, "type = " + Sms.MESSAGE_TYPE_OUTBOX, null);
}
public static final String CLASS_ZERO_BODY_KEY = "CLASS_ZERO_BODY";
// This must match the column IDs below.
private final static String[] REPLACE_PROJECTION = new String[] {
Sms._ID,
Sms.ADDRESS,
Sms.PROTOCOL
};
// This must match REPLACE_PROJECTION.
private static final int REPLACE_COLUMN_ID = 0;
/**
* If the message is a class-zero message, display it immediately
* and return null. Otherwise, store it using the
* ContentResolver
and return the
* Uri
of the thread containing this message
* so that we can use it for notification.
*/
private Uri insertMessage(Context context, SmsMessage[] msgs) {
// Build the helper classes to parse the messages.
SmsMessage sms = msgs[0];
if (sms.getMessageClass() == SmsMessage.MessageClass.CLASS_0) {
displayClassZeroMessage(context, sms);
return null;
} else if (sms.isReplace()) {
return replaceMessage(context, msgs);
} else {
return storeMessage(context, msgs);
}
}
/**
* This method is used if this is a "replace short message" SMS.
* We find any existing message that matches the incoming
* message's originating address and protocol identifier. If
* there is one, we replace its fields with those of the new
* message. Otherwise, we store the new message as usual.
*
* See TS 23.040 9.2.3.9.
*/
private Uri replaceMessage(Context context, SmsMessage[] msgs) {
SmsMessage sms = msgs[0];
ContentValues values = extractContentValues(sms);
values.put(Inbox.BODY, sms.getMessageBody());
ContentResolver resolver = context.getContentResolver();
String originatingAddress = sms.getOriginatingAddress();
int protocolIdentifier = sms.getProtocolIdentifier();
String selection =
Sms.ADDRESS + " = ? AND " +
Sms.PROTOCOL + " = ?";
String[] selectionArgs = new String[] {
originatingAddress, Integer.toString(protocolIdentifier)
};
Cursor cursor = SqliteWrapper.query(context, resolver, Inbox.CONTENT_URI,
REPLACE_PROJECTION, selection, selectionArgs, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
long messageId = cursor.getLong(REPLACE_COLUMN_ID);
Uri messageUri = ContentUris.withAppendedId(
Sms.CONTENT_URI, messageId);
SqliteWrapper.update(context, resolver, messageUri,
values, null, null);
return messageUri;
}
} finally {
cursor.close();
}
}
return storeMessage(context, msgs);
}
private Uri storeMessage(Context context, SmsMessage[] msgs) {
SmsMessage sms = msgs[0];
// Store the message in the content provider.
ContentValues values = extractContentValues(sms);
int pduCount = msgs.length;
if (pduCount == 1) {
// There is only one part, so grab the body directly.
values.put(Inbox.BODY, sms.getDisplayMessageBody());
} else {
// Build up the body from the parts.
StringBuilder body = new StringBuilder();
for (int i = 0; i < pduCount; i++) {
sms = msgs[i];
body.append(sms.getDisplayMessageBody());
}
values.put(Inbox.BODY, body.toString());
}
ContentResolver resolver = context.getContentResolver();
return SqliteWrapper.insert(context, resolver, Inbox.CONTENT_URI, values);
}
/**
* Extract all the content values except the body from an SMS
* message.
*/
private ContentValues extractContentValues(SmsMessage sms) {
// Store the message in the content provider.
ContentValues values = new ContentValues();
values.put(Inbox.ADDRESS, sms.getDisplayOriginatingAddress());
// Use now for the timestamp to avoid confusion with clock
// drift between the handset and the SMSC.
values.put(Inbox.DATE, new Long(System.currentTimeMillis()));
values.put(Inbox.PROTOCOL, sms.getProtocolIdentifier());
values.put(Inbox.READ, Integer.valueOf(0));
if (sms.getPseudoSubject().length() > 0) {
values.put(Inbox.SUBJECT, sms.getPseudoSubject());
}
values.put(Inbox.REPLY_PATH_PRESENT, sms.isReplyPathPresent() ? 1 : 0);
values.put(Inbox.SERVICE_CENTER, sms.getServiceCenterAddress());
return values;
}
/**
* Displays a class-zero message immediately in a pop-up window
* with the number from where it received the Notification with
* the body of the message
*
*/
private void displayClassZeroMessage(Context context, SmsMessage sms) {
// Using NEW_TASK here is necessary because we're calling
// startActivity from outside an activity.
Intent smsDialogIntent = new Intent(context, ClassZeroActivity.class)
.putExtra(CLASS_ZERO_BODY_KEY, sms.getMessageBody())
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
context.startActivity(smsDialogIntent);
}
}