| /* |
| * Copyright (C) 2015 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.messaging.datamodel.action; |
| |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.provider.Telephony.Mms; |
| import android.provider.Telephony.Sms; |
| |
| import com.android.messaging.Factory; |
| import com.android.messaging.datamodel.BugleDatabaseOperations; |
| import com.android.messaging.datamodel.DataModel; |
| import com.android.messaging.datamodel.DatabaseHelper.MessageColumns; |
| import com.android.messaging.datamodel.DatabaseWrapper; |
| import com.android.messaging.datamodel.MessagingContentProvider; |
| import com.android.messaging.datamodel.SyncManager; |
| import com.android.messaging.datamodel.data.MessageData; |
| import com.android.messaging.datamodel.data.ParticipantData; |
| import com.android.messaging.sms.MmsUtils; |
| import com.android.messaging.util.Assert; |
| import com.android.messaging.util.LogUtil; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * Action used to send an outgoing message. It writes MMS messages to the telephony db |
| * ({@link InsertNewMessageAction}) writes SMS messages to the telephony db). It also |
| * initiates the actual sending. It will all be used for re-sending a failed message. |
| * <p> |
| * This class is public (not package-private) because the SMS/MMS (e.g. MmsUtils) classes need to |
| * access the EXTRA_* fields for setting up the 'sent' pending intent. |
| */ |
| public class SendMessageAction extends Action implements Parcelable { |
| private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG; |
| |
| /** |
| * Queue sending of existing message (can only be called during execute of action) |
| */ |
| static boolean queueForSendInBackground(final String messageId, |
| final Action processingAction) { |
| final SendMessageAction action = new SendMessageAction(); |
| return action.queueAction(messageId, processingAction); |
| } |
| |
| public static final boolean DEFAULT_DELIVERY_REPORT_MODE = false; |
| public static final int MAX_SMS_RETRY = 3; |
| |
| // Core parameters needed for all types of message |
| private static final String KEY_MESSAGE_ID = "message_id"; |
| private static final String KEY_MESSAGE = "message"; |
| private static final String KEY_MESSAGE_URI = "message_uri"; |
| private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number"; |
| |
| // For sms messages a few extra values are included in the bundle |
| private static final String KEY_RECIPIENT = "recipient"; |
| private static final String KEY_RECIPIENTS = "recipients"; |
| private static final String KEY_SMS_SERVICE_CENTER = "sms_service_center"; |
| |
| // Values we attach to the pending intent that's fired when the message is sent. |
| // Only applicable when sending via the platform APIs on L+. |
| public static final String KEY_SUB_ID = "sub_id"; |
| public static final String EXTRA_MESSAGE_ID = "message_id"; |
| public static final String EXTRA_UPDATED_MESSAGE_URI = "updated_message_uri"; |
| public static final String EXTRA_CONTENT_URI = "content_uri"; |
| public static final String EXTRA_RESPONSE_IMPORTANT = "response_important"; |
| |
| /** |
| * Constructor used for retrying sending in the background (only message id available) |
| */ |
| private SendMessageAction() { |
| super(); |
| } |
| |
| /** |
| * Read message from database and queue actual sending |
| */ |
| private boolean queueAction(final String messageId, final Action processingAction) { |
| actionParameters.putString(KEY_MESSAGE_ID, messageId); |
| |
| final DatabaseWrapper db = DataModel.get().getDatabase(); |
| |
| final MessageData message = BugleDatabaseOperations.readMessage(db, messageId); |
| // Check message can be resent |
| if (message != null && message.canSendMessage()) { |
| final boolean isSms = message.getIsSms(); |
| long timestamp = System.currentTimeMillis(); |
| if (!isSms) { |
| // MMS expects timestamp rounded to nearest second |
| timestamp = 1000 * ((timestamp + 500) / 1000); |
| } |
| |
| final ParticipantData self = BugleDatabaseOperations.getExistingParticipant( |
| db, message.getSelfId()); |
| final Uri messageUri = message.getSmsMessageUri(); |
| final String conversationId = message.getConversationId(); |
| |
| // Update message status |
| if (message.getYetToSend()) { |
| if (message.getReceivedTimeStamp() == message.getRetryStartTimestamp()) { |
| // Initial sending of message |
| message.markMessageSending(timestamp); |
| } else { |
| // Manual resend of message |
| message.markMessageManualResend(timestamp); |
| } |
| } else { |
| // Automatic resend of message |
| message.markMessageResending(timestamp); |
| } |
| if (!updateMessageAndStatus(isSms, message, null /* messageUri */, false /*notify*/)) { |
| // If message is missing in the telephony database we don't need to send it |
| return false; |
| } |
| |
| final ArrayList<String> recipients = |
| BugleDatabaseOperations.getRecipientsForConversation(db, conversationId); |
| |
| // Update action state with parameters needed for background sending |
| actionParameters.putParcelable(KEY_MESSAGE_URI, messageUri); |
| actionParameters.putParcelable(KEY_MESSAGE, message); |
| actionParameters.putStringArrayList(KEY_RECIPIENTS, recipients); |
| actionParameters.putInt(KEY_SUB_ID, self.getSubId()); |
| actionParameters.putString(KEY_SUB_PHONE_NUMBER, self.getNormalizedDestination()); |
| |
| if (isSms) { |
| final String smsc = BugleDatabaseOperations.getSmsServiceCenterForConversation( |
| db, conversationId); |
| actionParameters.putString(KEY_SMS_SERVICE_CENTER, smsc); |
| |
| if (recipients.size() == 1) { |
| final String recipient = recipients.get(0); |
| |
| actionParameters.putString(KEY_RECIPIENT, recipient); |
| // Queue actual sending for SMS |
| processingAction.requestBackgroundWork(this); |
| |
| if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { |
| LogUtil.d(TAG, "SendMessageAction: Queued SMS message " + messageId |
| + " for sending"); |
| } |
| return true; |
| } else { |
| LogUtil.wtf(TAG, "Trying to resend a broadcast SMS - not allowed"); |
| } |
| } else { |
| // Queue actual sending for MMS |
| processingAction.requestBackgroundWork(this); |
| |
| if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { |
| LogUtil.d(TAG, "SendMessageAction: Queued MMS message " + messageId |
| + " for sending"); |
| } |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| |
| /** |
| * Never called |
| */ |
| @Override |
| protected Object executeAction() { |
| Assert.fail("SendMessageAction must be queued rather than started"); |
| return null; |
| } |
| |
| /** |
| * Send message on background worker thread |
| */ |
| @Override |
| protected Bundle doBackgroundWork() { |
| final MessageData message = actionParameters.getParcelable(KEY_MESSAGE); |
| final String messageId = actionParameters.getString(KEY_MESSAGE_ID); |
| Uri messageUri = actionParameters.getParcelable(KEY_MESSAGE_URI); |
| Uri updatedMessageUri = null; |
| final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS; |
| final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID); |
| final String subPhoneNumber = actionParameters.getString(KEY_SUB_PHONE_NUMBER); |
| |
| LogUtil.i(TAG, "SendMessageAction: Sending " + (isSms ? "SMS" : "MMS") + " message " |
| + messageId + " in conversation " + message.getConversationId()); |
| |
| int status; |
| int rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED; |
| int resultCode = MessageData.UNKNOWN_RESULT_CODE; |
| if (isSms) { |
| Assert.notNull(messageUri); |
| final String recipient = actionParameters.getString(KEY_RECIPIENT); |
| final String messageText = message.getMessageText(); |
| final String smsServiceCenter = actionParameters.getString(KEY_SMS_SERVICE_CENTER); |
| final boolean deliveryReportRequired = MmsUtils.isDeliveryReportRequired(subId); |
| |
| status = MmsUtils.sendSmsMessage(recipient, messageText, messageUri, subId, |
| smsServiceCenter, deliveryReportRequired); |
| } else { |
| final Context context = Factory.get().getApplicationContext(); |
| final ArrayList<String> recipients = |
| actionParameters.getStringArrayList(KEY_RECIPIENTS); |
| if (messageUri == null) { |
| final long timestamp = message.getReceivedTimeStamp(); |
| |
| // Inform sync that message has been added at local received timestamp |
| final SyncManager syncManager = DataModel.get().getSyncManager(); |
| syncManager.onNewMessageInserted(timestamp); |
| |
| // For MMS messages first need to write to telephony (resizing images if needed) |
| updatedMessageUri = MmsUtils.insertSendingMmsMessage(context, recipients, |
| message, subId, subPhoneNumber, timestamp); |
| if (updatedMessageUri != null) { |
| messageUri = updatedMessageUri; |
| // To prevent Sync seeing inconsistent state must write to DB on this thread |
| updateMessageUri(messageId, updatedMessageUri); |
| |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "SendMessageAction: Updated message " + messageId |
| + " with new uri " + messageUri); |
| } |
| } |
| } |
| if (messageUri != null) { |
| // Actually send the MMS |
| final Bundle extras = new Bundle(); |
| extras.putString(EXTRA_MESSAGE_ID, messageId); |
| extras.putParcelable(EXTRA_UPDATED_MESSAGE_URI, updatedMessageUri); |
| final MmsUtils.StatusPlusUri result = MmsUtils.sendMmsMessage(context, subId, |
| messageUri, extras); |
| if (result == MmsUtils.STATUS_PENDING) { |
| // Async send, so no status yet |
| LogUtil.d(TAG, "SendMessageAction: Sending MMS message " + messageId |
| + " asynchronously; waiting for callback to finish processing"); |
| return null; |
| } |
| status = result.status; |
| rawStatus = result.rawStatus; |
| resultCode = result.resultCode; |
| } else { |
| status = MmsUtils.MMS_REQUEST_MANUAL_RETRY; |
| } |
| } |
| |
| // When we fast-fail before calling the MMS lib APIs (e.g. airplane mode, |
| // sending message is deleted). |
| ProcessSentMessageAction.processMessageSentFastFailed(messageId, messageUri, |
| updatedMessageUri, subId, isSms, status, rawStatus, resultCode); |
| return null; |
| } |
| |
| private void updateMessageUri(final String messageId, final Uri updatedMessageUri) { |
| final DatabaseWrapper db = DataModel.get().getDatabase(); |
| db.beginTransaction(); |
| try { |
| final ContentValues values = new ContentValues(); |
| values.put(MessageColumns.SMS_MESSAGE_URI, updatedMessageUri.toString()); |
| BugleDatabaseOperations.updateMessageRow(db, messageId, values); |
| db.setTransactionSuccessful(); |
| } finally { |
| db.endTransaction(); |
| } |
| } |
| |
| @Override |
| protected Object processBackgroundResponse(final Bundle response) { |
| // Nothing to do here, post-send tasks handled by ProcessSentMessageAction |
| return null; |
| } |
| |
| /** |
| * Update message status to reflect success or failure |
| */ |
| @Override |
| protected Object processBackgroundFailure() { |
| final String messageId = actionParameters.getString(KEY_MESSAGE_ID); |
| final MessageData message = actionParameters.getParcelable(KEY_MESSAGE); |
| final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS; |
| final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID); |
| final int resultCode = actionParameters.getInt(ProcessSentMessageAction.KEY_RESULT_CODE); |
| final int httpStatusCode = |
| actionParameters.getInt(ProcessSentMessageAction.KEY_HTTP_STATUS_CODE); |
| |
| ProcessSentMessageAction.processResult(messageId, null /* updatedMessageUri */, |
| MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED, |
| isSms, this, subId, resultCode, httpStatusCode); |
| |
| return null; |
| } |
| |
| /** |
| * Update the message status (and message itself if necessary) |
| * @param isSms whether this is an SMS or MMS |
| * @param message message to update |
| * @param updatedMessageUri message uri for newly-inserted messages; null otherwise |
| * @param clearSeen whether the message 'seen' status should be reset if error occurs |
| */ |
| public static boolean updateMessageAndStatus(final boolean isSms, final MessageData message, |
| final Uri updatedMessageUri, final boolean clearSeen) { |
| final Context context = Factory.get().getApplicationContext(); |
| final DatabaseWrapper db = DataModel.get().getDatabase(); |
| |
| // TODO: We're optimistically setting the type/box of outgoing messages to |
| // 'SENT' even before they actually are. We should technically be using QUEUED or OUTBOX |
| // instead, but if we do that, it's possible that the Messaging app will try to send them |
| // as part of its clean-up logic that runs when it starts (http://b/18155366). |
| // |
| // We also use the wrong status when inserting queued SMS messages in |
| // InsertNewMessageAction.insertBroadcastSmsMessage and insertSendingSmsMessage (should be |
| // QUEUED or OUTBOX), and in MmsUtils.insertSendReq (should be OUTBOX). |
| |
| boolean updatedTelephony = true; |
| int messageBox; |
| int type; |
| switch(message.getStatus()) { |
| case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE: |
| case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED: |
| type = Sms.MESSAGE_TYPE_SENT; |
| messageBox = Mms.MESSAGE_BOX_SENT; |
| break; |
| case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND: |
| case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY: |
| type = Sms.MESSAGE_TYPE_SENT; |
| messageBox = Mms.MESSAGE_BOX_SENT; |
| break; |
| case MessageData.BUGLE_STATUS_OUTGOING_SENDING: |
| case MessageData.BUGLE_STATUS_OUTGOING_RESENDING: |
| type = Sms.MESSAGE_TYPE_SENT; |
| messageBox = Mms.MESSAGE_BOX_SENT; |
| break; |
| case MessageData.BUGLE_STATUS_OUTGOING_FAILED: |
| case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER: |
| type = Sms.MESSAGE_TYPE_FAILED; |
| messageBox = Mms.MESSAGE_BOX_FAILED; |
| break; |
| default: |
| type = Sms.MESSAGE_TYPE_ALL; |
| messageBox = Mms.MESSAGE_BOX_ALL; |
| break; |
| } |
| // First in the telephony DB |
| if (isSms) { |
| // Ignore update message Uri |
| if (type != Sms.MESSAGE_TYPE_ALL) { |
| if (!MmsUtils.updateSmsMessageSendingStatus(context, message.getSmsMessageUri(), |
| type, message.getReceivedTimeStamp())) { |
| message.markMessageFailed(message.getSentTimeStamp()); |
| updatedTelephony = false; |
| } |
| } |
| } else if (message.getSmsMessageUri() != null) { |
| if (messageBox != Mms.MESSAGE_BOX_ALL) { |
| if (!MmsUtils.updateMmsMessageSendingStatus(context, message.getSmsMessageUri(), |
| messageBox, message.getReceivedTimeStamp())) { |
| message.markMessageFailed(message.getSentTimeStamp()); |
| updatedTelephony = false; |
| } |
| } |
| } |
| if (updatedTelephony) { |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS") |
| + " message " + message.getMessageId() |
| + " in telephony (" + message.getSmsMessageUri() + ")"); |
| } |
| } else { |
| LogUtil.w(TAG, "SendMessageAction: Failed to update " + (isSms ? "SMS" : "MMS") |
| + " message " + message.getMessageId() |
| + " in telephony (" + message.getSmsMessageUri() + "); marking message failed"); |
| } |
| |
| // Update the local DB |
| db.beginTransaction(); |
| try { |
| if (updatedMessageUri != null) { |
| // Update all message and part fields |
| BugleDatabaseOperations.updateMessageInTransaction(db, message); |
| BugleDatabaseOperations.refreshConversationMetadataInTransaction( |
| db, message.getConversationId(), false/* shouldAutoSwitchSelfId */, |
| false/*archived*/); |
| } else { |
| final ContentValues values = new ContentValues(); |
| values.put(MessageColumns.STATUS, message.getStatus()); |
| |
| if (clearSeen) { |
| // When a message fails to send, the message needs to |
| // be unseen to be selected as an error notification. |
| values.put(MessageColumns.SEEN, 0); |
| } |
| values.put(MessageColumns.RECEIVED_TIMESTAMP, message.getReceivedTimeStamp()); |
| values.put(MessageColumns.RAW_TELEPHONY_STATUS, message.getRawTelephonyStatus()); |
| |
| BugleDatabaseOperations.updateMessageRowIfExists(db, message.getMessageId(), |
| values); |
| } |
| db.setTransactionSuccessful(); |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS") |
| + " message " + message.getMessageId() + " in local db. Timestamp = " |
| + message.getReceivedTimeStamp()); |
| } |
| } finally { |
| db.endTransaction(); |
| } |
| |
| MessagingContentProvider.notifyMessagesChanged(message.getConversationId()); |
| if (updatedMessageUri != null) { |
| MessagingContentProvider.notifyPartsChanged(); |
| } |
| |
| return updatedTelephony; |
| } |
| |
| private SendMessageAction(final Parcel in) { |
| super(in); |
| } |
| |
| public static final Parcelable.Creator<SendMessageAction> CREATOR |
| = new Parcelable.Creator<SendMessageAction>() { |
| @Override |
| public SendMessageAction createFromParcel(final Parcel in) { |
| return new SendMessageAction(in); |
| } |
| |
| @Override |
| public SendMessageAction[] newArray(final int size) { |
| return new SendMessageAction[size]; |
| } |
| }; |
| |
| @Override |
| public void writeToParcel(final Parcel parcel, final int flags) { |
| writeActionToParcel(parcel, flags); |
| } |
| } |