Home | History | Annotate | Download | only in action
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.messaging.datamodel.action;
     18 
     19 import android.content.ContentValues;
     20 import android.content.Context;
     21 import android.net.Uri;
     22 import android.os.Bundle;
     23 import android.os.Parcel;
     24 import android.os.Parcelable;
     25 import android.provider.Telephony.Mms;
     26 import android.provider.Telephony.Sms;
     27 
     28 import com.android.messaging.Factory;
     29 import com.android.messaging.datamodel.BugleDatabaseOperations;
     30 import com.android.messaging.datamodel.DataModel;
     31 import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
     32 import com.android.messaging.datamodel.DatabaseWrapper;
     33 import com.android.messaging.datamodel.MessagingContentProvider;
     34 import com.android.messaging.datamodel.SyncManager;
     35 import com.android.messaging.datamodel.data.MessageData;
     36 import com.android.messaging.datamodel.data.ParticipantData;
     37 import com.android.messaging.sms.MmsUtils;
     38 import com.android.messaging.util.Assert;
     39 import com.android.messaging.util.LogUtil;
     40 
     41 import java.util.ArrayList;
     42 
     43 /**
     44  * Action used to send an outgoing message. It writes MMS messages to the telephony db
     45  * ({@link InsertNewMessageAction}) writes SMS messages to the telephony db). It also
     46  * initiates the actual sending. It will all be used for re-sending a failed message.
     47  * NOTE: This action must queue a ProcessPendingMessagesAction when it is done (success or failure).
     48  * <p>
     49  * This class is public (not package-private) because the SMS/MMS (e.g. MmsUtils) classes need to
     50  * access the EXTRA_* fields for setting up the 'sent' pending intent.
     51  */
     52 public class SendMessageAction extends Action implements Parcelable {
     53     private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
     54 
     55     /**
     56      * Queue sending of existing message (can only be called during execute of action)
     57      */
     58     static boolean queueForSendInBackground(final String messageId,
     59             final Action processingAction) {
     60         final SendMessageAction action = new SendMessageAction();
     61         return action.queueAction(messageId, processingAction);
     62     }
     63 
     64     public static final boolean DEFAULT_DELIVERY_REPORT_MODE  = false;
     65     public static final int MAX_SMS_RETRY = 3;
     66 
     67     // Core parameters needed for all types of message
     68     private static final String KEY_MESSAGE_ID = "message_id";
     69     private static final String KEY_MESSAGE = "message";
     70     private static final String KEY_MESSAGE_URI = "message_uri";
     71     private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number";
     72 
     73     // For sms messages a few extra values are included in the bundle
     74     private static final String KEY_RECIPIENT = "recipient";
     75     private static final String KEY_RECIPIENTS = "recipients";
     76     private static final String KEY_SMS_SERVICE_CENTER = "sms_service_center";
     77 
     78     // Values we attach to the pending intent that's fired when the message is sent.
     79     // Only applicable when sending via the platform APIs on L+.
     80     public static final String KEY_SUB_ID = "sub_id";
     81     public static final String EXTRA_MESSAGE_ID = "message_id";
     82     public static final String EXTRA_UPDATED_MESSAGE_URI = "updated_message_uri";
     83     public static final String EXTRA_CONTENT_URI = "content_uri";
     84     public static final String EXTRA_RESPONSE_IMPORTANT = "response_important";
     85 
     86     /**
     87      * Constructor used for retrying sending in the background (only message id available)
     88      */
     89     private SendMessageAction() {
     90         super();
     91     }
     92 
     93     /**
     94      * Read message from database and queue actual sending
     95      */
     96     private boolean queueAction(final String messageId, final Action processingAction) {
     97         actionParameters.putString(KEY_MESSAGE_ID, messageId);
     98 
     99         final long timestamp = System.currentTimeMillis();
    100         final DatabaseWrapper db = DataModel.get().getDatabase();
    101 
    102         final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
    103         // Check message can be resent
    104         if (message != null && message.canSendMessage()) {
    105             final boolean isSms = (message.getProtocol() == MessageData.PROTOCOL_SMS);
    106 
    107             final ParticipantData self = BugleDatabaseOperations.getExistingParticipant(
    108                     db, message.getSelfId());
    109             final Uri messageUri = message.getSmsMessageUri();
    110             final String conversationId = message.getConversationId();
    111 
    112             // Update message status
    113             if (message.getYetToSend()) {
    114                 // Initial sending of message
    115                 message.markMessageSending(timestamp);
    116             } else {
    117                 // Automatic resend of message
    118                 message.markMessageResending(timestamp);
    119             }
    120             if (!updateMessageAndStatus(isSms, message, null /* messageUri */, false /*notify*/)) {
    121                 // If message is missing in the telephony database we don't need to send it
    122                 return false;
    123             }
    124 
    125             final ArrayList<String> recipients =
    126                     BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);
    127 
    128             // Update action state with parameters needed for background sending
    129             actionParameters.putParcelable(KEY_MESSAGE_URI, messageUri);
    130             actionParameters.putParcelable(KEY_MESSAGE, message);
    131             actionParameters.putStringArrayList(KEY_RECIPIENTS, recipients);
    132             actionParameters.putInt(KEY_SUB_ID, self.getSubId());
    133             actionParameters.putString(KEY_SUB_PHONE_NUMBER, self.getNormalizedDestination());
    134 
    135             if (isSms) {
    136                 final String smsc = BugleDatabaseOperations.getSmsServiceCenterForConversation(
    137                         db, conversationId);
    138                 actionParameters.putString(KEY_SMS_SERVICE_CENTER, smsc);
    139 
    140                 if (recipients.size() == 1) {
    141                     final String recipient = recipients.get(0);
    142 
    143                     actionParameters.putString(KEY_RECIPIENT, recipient);
    144                     // Queue actual sending for SMS
    145                     processingAction.requestBackgroundWork(this);
    146 
    147                     if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    148                         LogUtil.d(TAG, "SendMessageAction: Queued SMS message " + messageId
    149                                 + " for sending");
    150                     }
    151                     return true;
    152                 } else {
    153                     LogUtil.wtf(TAG, "Trying to resend a broadcast SMS - not allowed");
    154                 }
    155             } else {
    156                 // Queue actual sending for MMS
    157                 processingAction.requestBackgroundWork(this);
    158 
    159                 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    160                     LogUtil.d(TAG, "SendMessageAction: Queued MMS message " + messageId
    161                             + " for sending");
    162                 }
    163                 return true;
    164             }
    165         }
    166 
    167         return false;
    168     }
    169 
    170 
    171     /**
    172      * Never called
    173      */
    174     @Override
    175     protected Object executeAction() {
    176         Assert.fail("SendMessageAction must be queued rather than started");
    177         return null;
    178     }
    179 
    180     /**
    181      * Send message on background worker thread
    182      */
    183     @Override
    184     protected Bundle doBackgroundWork() {
    185         final MessageData message = actionParameters.getParcelable(KEY_MESSAGE);
    186         final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
    187         Uri messageUri = actionParameters.getParcelable(KEY_MESSAGE_URI);
    188         Uri updatedMessageUri = null;
    189         final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS;
    190         final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
    191         final String subPhoneNumber = actionParameters.getString(KEY_SUB_PHONE_NUMBER);
    192 
    193         LogUtil.i(TAG, "SendMessageAction: Sending " + (isSms ? "SMS" : "MMS") + " message "
    194                 + messageId + " in conversation " + message.getConversationId());
    195 
    196         int status;
    197         int rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED;
    198         int resultCode = MessageData.UNKNOWN_RESULT_CODE;
    199         if (isSms) {
    200             Assert.notNull(messageUri);
    201             final String recipient = actionParameters.getString(KEY_RECIPIENT);
    202             final String messageText = message.getMessageText();
    203             final String smsServiceCenter = actionParameters.getString(KEY_SMS_SERVICE_CENTER);
    204             final boolean deliveryReportRequired = MmsUtils.isDeliveryReportRequired(subId);
    205 
    206             status = MmsUtils.sendSmsMessage(recipient, messageText, messageUri, subId,
    207                     smsServiceCenter, deliveryReportRequired);
    208         } else {
    209             final Context context = Factory.get().getApplicationContext();
    210             final ArrayList<String> recipients =
    211                     actionParameters.getStringArrayList(KEY_RECIPIENTS);
    212             if (messageUri == null) {
    213                 final long timestamp = message.getReceivedTimeStamp();
    214 
    215                 // Inform sync that message has been added at local received timestamp
    216                 final SyncManager syncManager = DataModel.get().getSyncManager();
    217                 syncManager.onNewMessageInserted(timestamp);
    218 
    219                 // For MMS messages first need to write to telephony (resizing images if needed)
    220                 updatedMessageUri = MmsUtils.insertSendingMmsMessage(context, recipients,
    221                         message, subId, subPhoneNumber, timestamp);
    222                 if (updatedMessageUri != null) {
    223                     messageUri = updatedMessageUri;
    224                     // To prevent Sync seeing inconsistent state must write to DB on this thread
    225                     updateMessageUri(messageId, updatedMessageUri);
    226 
    227                     if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    228                         LogUtil.v(TAG, "SendMessageAction: Updated message " + messageId
    229                                 + " with new uri " + messageUri);
    230                     }
    231                  }
    232             }
    233             if (messageUri != null) {
    234                 // Actually send the MMS
    235                 final Bundle extras = new Bundle();
    236                 extras.putString(EXTRA_MESSAGE_ID, messageId);
    237                 extras.putParcelable(EXTRA_UPDATED_MESSAGE_URI, updatedMessageUri);
    238                 final MmsUtils.StatusPlusUri result = MmsUtils.sendMmsMessage(context, subId,
    239                         messageUri, extras);
    240                 if (result == MmsUtils.STATUS_PENDING) {
    241                     // Async send, so no status yet
    242                     LogUtil.d(TAG, "SendMessageAction: Sending MMS message " + messageId
    243                             + " asynchronously; waiting for callback to finish processing");
    244                     return null;
    245                 }
    246                 status = result.status;
    247                 rawStatus = result.rawStatus;
    248                 resultCode = result.resultCode;
    249             } else {
    250                 status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
    251             }
    252         }
    253 
    254         // When we fast-fail before calling the MMS lib APIs (e.g. airplane mode,
    255         // sending message is deleted).
    256         ProcessSentMessageAction.processMessageSentFastFailed(messageId, messageUri,
    257                 updatedMessageUri, subId, isSms, status, rawStatus, resultCode);
    258         return null;
    259     }
    260 
    261     private void updateMessageUri(final String messageId, final Uri updatedMessageUri) {
    262         final DatabaseWrapper db = DataModel.get().getDatabase();
    263         db.beginTransaction();
    264         try {
    265             final ContentValues values = new ContentValues();
    266             values.put(MessageColumns.SMS_MESSAGE_URI, updatedMessageUri.toString());
    267             BugleDatabaseOperations.updateMessageRow(db, messageId, values);
    268             db.setTransactionSuccessful();
    269         } finally {
    270             db.endTransaction();
    271         }
    272     }
    273 
    274     @Override
    275     protected Object processBackgroundResponse(final Bundle response) {
    276         // Nothing to do here, post-send tasks handled by ProcessSentMessageAction
    277         return null;
    278     }
    279 
    280     /**
    281      * Update message status to reflect success or failure
    282      */
    283     @Override
    284     protected Object processBackgroundFailure() {
    285         final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
    286         final MessageData message = actionParameters.getParcelable(KEY_MESSAGE);
    287         final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS;
    288         final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
    289         final int resultCode = actionParameters.getInt(ProcessSentMessageAction.KEY_RESULT_CODE);
    290         final int httpStatusCode =
    291                 actionParameters.getInt(ProcessSentMessageAction.KEY_HTTP_STATUS_CODE);
    292 
    293         ProcessSentMessageAction.processResult(messageId, null /* updatedMessageUri */,
    294                 MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED,
    295                 isSms, this, subId, resultCode, httpStatusCode);
    296 
    297         // Whether we succeeded or failed we will check and maybe schedule some more work
    298         ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(true, this);
    299 
    300         return null;
    301     }
    302 
    303     /**
    304      * Update the message status (and message itself if necessary)
    305      * @param isSms whether this is an SMS or MMS
    306      * @param message message to update
    307      * @param updatedMessageUri message uri for newly-inserted messages; null otherwise
    308      * @param clearSeen whether the message 'seen' status should be reset if error occurs
    309      */
    310     public static boolean updateMessageAndStatus(final boolean isSms, final MessageData message,
    311             final Uri updatedMessageUri, final boolean clearSeen) {
    312         final Context context = Factory.get().getApplicationContext();
    313         final DatabaseWrapper db = DataModel.get().getDatabase();
    314 
    315         // TODO: We're optimistically setting the type/box of outgoing messages to
    316         // 'SENT' even before they actually are. We should technically be using QUEUED or OUTBOX
    317         // instead, but if we do that, it's possible that the Messaging app will try to send them
    318         // as part of its clean-up logic that runs when it starts (http://b/18155366).
    319         //
    320         // We also use the wrong status when inserting queued SMS messages in
    321         // InsertNewMessageAction.insertBroadcastSmsMessage and insertSendingSmsMessage (should be
    322         // QUEUED or OUTBOX), and in MmsUtils.insertSendReq (should be OUTBOX).
    323 
    324         boolean updatedTelephony = true;
    325         int messageBox;
    326         int type;
    327         switch(message.getStatus()) {
    328             case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE:
    329             case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED:
    330                 type = Sms.MESSAGE_TYPE_SENT;
    331                 messageBox = Mms.MESSAGE_BOX_SENT;
    332                 break;
    333             case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND:
    334             case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
    335                 type = Sms.MESSAGE_TYPE_SENT;
    336                 messageBox = Mms.MESSAGE_BOX_SENT;
    337                 break;
    338             case MessageData.BUGLE_STATUS_OUTGOING_SENDING:
    339             case MessageData.BUGLE_STATUS_OUTGOING_RESENDING:
    340                 type = Sms.MESSAGE_TYPE_SENT;
    341                 messageBox = Mms.MESSAGE_BOX_SENT;
    342                 break;
    343             case MessageData.BUGLE_STATUS_OUTGOING_FAILED:
    344             case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
    345                 type = Sms.MESSAGE_TYPE_FAILED;
    346                 messageBox = Mms.MESSAGE_BOX_FAILED;
    347                 break;
    348             default:
    349                 type = Sms.MESSAGE_TYPE_ALL;
    350                 messageBox = Mms.MESSAGE_BOX_ALL;
    351                 break;
    352         }
    353         // First in the telephony DB
    354         if (isSms) {
    355             // Ignore update message Uri
    356             if (type != Sms.MESSAGE_TYPE_ALL) {
    357                 if (!MmsUtils.updateSmsMessageSendingStatus(context, message.getSmsMessageUri(),
    358                         type, message.getReceivedTimeStamp())) {
    359                     message.markMessageFailed(message.getSentTimeStamp());
    360                     updatedTelephony = false;
    361                 }
    362             }
    363         } else if (message.getSmsMessageUri() != null) {
    364             if (messageBox != Mms.MESSAGE_BOX_ALL) {
    365                 if (!MmsUtils.updateMmsMessageSendingStatus(context, message.getSmsMessageUri(),
    366                         messageBox, message.getReceivedTimeStamp())) {
    367                     message.markMessageFailed(message.getSentTimeStamp());
    368                     updatedTelephony = false;
    369                 }
    370             }
    371         }
    372         if (updatedTelephony) {
    373             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    374                 LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS")
    375                         + " message " + message.getMessageId()
    376                         + " in telephony (" + message.getSmsMessageUri() + ")");
    377             }
    378         } else {
    379             LogUtil.w(TAG, "SendMessageAction: Failed to update " + (isSms ? "SMS" : "MMS")
    380                     + " message " + message.getMessageId()
    381                     + " in telephony (" + message.getSmsMessageUri() + "); marking message failed");
    382         }
    383 
    384         // Update the local DB
    385         db.beginTransaction();
    386         try {
    387             if (updatedMessageUri != null) {
    388                 // Update all message and part fields
    389                 BugleDatabaseOperations.updateMessageInTransaction(db, message);
    390                 BugleDatabaseOperations.refreshConversationMetadataInTransaction(
    391                         db, message.getConversationId(), false/* shouldAutoSwitchSelfId */,
    392                         false/*archived*/);
    393             } else {
    394                 final ContentValues values = new ContentValues();
    395                 values.put(MessageColumns.STATUS, message.getStatus());
    396 
    397                 if (clearSeen) {
    398                     // When a message fails to send, the message needs to
    399                     // be unseen to be selected as an error notification.
    400                     values.put(MessageColumns.SEEN, 0);
    401                 }
    402                 values.put(MessageColumns.RECEIVED_TIMESTAMP, message.getReceivedTimeStamp());
    403                 values.put(MessageColumns.RAW_TELEPHONY_STATUS, message.getRawTelephonyStatus());
    404 
    405                 BugleDatabaseOperations.updateMessageRowIfExists(db, message.getMessageId(),
    406                         values);
    407             }
    408             db.setTransactionSuccessful();
    409             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    410                 LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS")
    411                         + " message " + message.getMessageId() + " in local db. Timestamp = "
    412                         + message.getReceivedTimeStamp());
    413             }
    414         } finally {
    415             db.endTransaction();
    416         }
    417 
    418         MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
    419         if (updatedMessageUri != null) {
    420             MessagingContentProvider.notifyPartsChanged();
    421         }
    422 
    423         return updatedTelephony;
    424     }
    425 
    426     private SendMessageAction(final Parcel in) {
    427         super(in);
    428     }
    429 
    430     public static final Parcelable.Creator<SendMessageAction> CREATOR
    431             = new Parcelable.Creator<SendMessageAction>() {
    432         @Override
    433         public SendMessageAction createFromParcel(final Parcel in) {
    434             return new SendMessageAction(in);
    435         }
    436 
    437         @Override
    438         public SendMessageAction[] newArray(final int size) {
    439             return new SendMessageAction[size];
    440         }
    441     };
    442 
    443     @Override
    444     public void writeToParcel(final Parcel parcel, final int flags) {
    445         writeActionToParcel(parcel, flags);
    446     }
    447 }
    448