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.Context;
     20 import android.net.Uri;
     21 import android.os.Parcel;
     22 import android.os.Parcelable;
     23 import android.provider.Telephony;
     24 import android.text.TextUtils;
     25 
     26 import com.android.messaging.Factory;
     27 import com.android.messaging.datamodel.BugleDatabaseOperations;
     28 import com.android.messaging.datamodel.DataModel;
     29 import com.android.messaging.datamodel.DatabaseWrapper;
     30 import com.android.messaging.datamodel.MessagingContentProvider;
     31 import com.android.messaging.datamodel.SyncManager;
     32 import com.android.messaging.datamodel.data.ConversationListItemData;
     33 import com.android.messaging.datamodel.data.MessageData;
     34 import com.android.messaging.datamodel.data.MessagePartData;
     35 import com.android.messaging.datamodel.data.ParticipantData;
     36 import com.android.messaging.sms.MmsUtils;
     37 import com.android.messaging.util.Assert;
     38 import com.android.messaging.util.LogUtil;
     39 import com.android.messaging.util.OsUtil;
     40 import com.android.messaging.util.PhoneUtils;
     41 
     42 import java.util.ArrayList;
     43 import java.util.List;
     44 
     45 /**
     46  * Action used to convert a draft message to an outgoing message. Its writes SMS messages to
     47  * the telephony db, but {@link SendMessageAction} is responsible for inserting MMS message into
     48  * the telephony DB. The latter also does the actual sending of the message in the background.
     49  * The latter is also responsible for re-sending a failed message.
     50  */
     51 public class InsertNewMessageAction extends Action implements Parcelable {
     52     private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
     53 
     54     private static long sLastSentMessageTimestamp = -1;
     55 
     56     /**
     57      * Insert message (no listener)
     58      */
     59     public static void insertNewMessage(final MessageData message) {
     60         final InsertNewMessageAction action = new InsertNewMessageAction(message);
     61         action.start();
     62     }
     63 
     64     /**
     65      * Insert message (no listener) with a given non-default subId.
     66      */
     67     public static void insertNewMessage(final MessageData message, final int subId) {
     68         Assert.isFalse(subId == ParticipantData.DEFAULT_SELF_SUB_ID);
     69         final InsertNewMessageAction action = new InsertNewMessageAction(message, subId);
     70         action.start();
     71     }
     72 
     73     /**
     74      * Insert message (no listener)
     75      */
     76     public static void insertNewMessage(final int subId, final String recipients,
     77             final String messageText, final String subject) {
     78         final InsertNewMessageAction action = new InsertNewMessageAction(
     79                 subId, recipients, messageText, subject);
     80         action.start();
     81     }
     82 
     83     public static long getLastSentMessageTimestamp() {
     84         return sLastSentMessageTimestamp;
     85     }
     86 
     87     private static final String KEY_SUB_ID = "sub_id";
     88     private static final String KEY_MESSAGE = "message";
     89     private static final String KEY_RECIPIENTS = "recipients";
     90     private static final String KEY_MESSAGE_TEXT = "message_text";
     91     private static final String KEY_SUBJECT_TEXT = "subject_text";
     92 
     93     private InsertNewMessageAction(final MessageData message) {
     94         this(message, ParticipantData.DEFAULT_SELF_SUB_ID);
     95         actionParameters.putParcelable(KEY_MESSAGE, message);
     96     }
     97 
     98     private InsertNewMessageAction(final MessageData message, final int subId) {
     99         super();
    100         actionParameters.putParcelable(KEY_MESSAGE, message);
    101         actionParameters.putInt(KEY_SUB_ID, subId);
    102     }
    103 
    104     private InsertNewMessageAction(final int subId, final String recipients,
    105             final String messageText, final String subject) {
    106         super();
    107         if (TextUtils.isEmpty(recipients) || TextUtils.isEmpty(messageText)) {
    108             Assert.fail("InsertNewMessageAction: Can't have empty recipients or message");
    109         }
    110         actionParameters.putInt(KEY_SUB_ID, subId);
    111         actionParameters.putString(KEY_RECIPIENTS, recipients);
    112         actionParameters.putString(KEY_MESSAGE_TEXT, messageText);
    113         actionParameters.putString(KEY_SUBJECT_TEXT, subject);
    114     }
    115 
    116     /**
    117      * Add message to database in pending state and queue actual sending
    118      */
    119     @Override
    120     protected Object executeAction() {
    121         LogUtil.i(TAG, "InsertNewMessageAction: inserting new message");
    122         MessageData message = actionParameters.getParcelable(KEY_MESSAGE);
    123         if (message == null) {
    124             LogUtil.i(TAG, "InsertNewMessageAction: Creating MessageData with provided data");
    125             message = createMessage();
    126             if (message == null) {
    127                 LogUtil.w(TAG, "InsertNewMessageAction: Could not create MessageData");
    128                 return null;
    129             }
    130         }
    131         final DatabaseWrapper db = DataModel.get().getDatabase();
    132         final String conversationId = message.getConversationId();
    133 
    134         final ParticipantData self = getSelf(db, conversationId, message);
    135         if (self == null) {
    136             return null;
    137         }
    138         message.bindSelfId(self.getId());
    139         // If the user taps the Send button before the conversation draft is created/loaded by
    140         // ReadDraftDataAction (maybe the action service thread was busy), the MessageData may not
    141         // have the participant id set. It should be equal to the self id, so we'll use that.
    142         if (message.getParticipantId() == null) {
    143             message.bindParticipantId(self.getId());
    144         }
    145 
    146         final long timestamp = System.currentTimeMillis();
    147         final ArrayList<String> recipients =
    148                 BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);
    149         if (recipients.size() < 1) {
    150             LogUtil.w(TAG, "InsertNewMessageAction: message recipients is empty");
    151             return null;
    152         }
    153         final int subId = self.getSubId();
    154 
    155         // TODO: Work out whether to send with SMS or MMS (taking into account recipients)?
    156         final boolean isSms = (message.getProtocol() == MessageData.PROTOCOL_SMS);
    157         if (isSms) {
    158             String sendingConversationId = conversationId;
    159             if (recipients.size() > 1) {
    160                 // Broadcast SMS - put message in "fake conversation" before farming out to real 1:1
    161                 final long laterTimestamp = timestamp + 1;
    162                 // Send a single message
    163                 insertBroadcastSmsMessage(conversationId, message, subId,
    164                         laterTimestamp, recipients);
    165 
    166                 sendingConversationId = null;
    167             }
    168 
    169             for (final String recipient : recipients) {
    170                 // Start actual sending
    171                 insertSendingSmsMessage(message, subId, recipient,
    172                         timestamp, sendingConversationId);
    173             }
    174 
    175             // Can now clear draft from conversation (deleting attachments if necessary)
    176             BugleDatabaseOperations.updateDraftMessageData(db, conversationId,
    177                     null /* message */, BugleDatabaseOperations.UPDATE_MODE_CLEAR_DRAFT);
    178         } else {
    179             final long timestampRoundedToSecond = 1000 * ((timestamp + 500) / 1000);
    180             // Write place holder message directly referencing parts from the draft
    181             final MessageData messageToSend = insertSendingMmsMessage(conversationId,
    182                     message, timestampRoundedToSecond);
    183 
    184             // Can now clear draft from conversation (preserving attachments which are now
    185             // referenced by messageToSend)
    186             BugleDatabaseOperations.updateDraftMessageData(db, conversationId,
    187                     messageToSend, BugleDatabaseOperations.UPDATE_MODE_CLEAR_DRAFT);
    188         }
    189         MessagingContentProvider.notifyConversationListChanged();
    190         ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
    191 
    192         return message;
    193     }
    194 
    195     private ParticipantData getSelf(
    196             final DatabaseWrapper db, final String conversationId, final MessageData message) {
    197         ParticipantData self;
    198         // Check if we are asked to bind to a non-default subId. This is directly passed in from
    199         // the UI thread so that the sub id may be locked as soon as the user clicks on the Send
    200         // button.
    201         final int requestedSubId = actionParameters.getInt(
    202                 KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
    203         if (requestedSubId != ParticipantData.DEFAULT_SELF_SUB_ID) {
    204             self = BugleDatabaseOperations.getOrCreateSelf(db, requestedSubId);
    205         } else {
    206             String selfId = message.getSelfId();
    207             if (selfId == null) {
    208                 // The conversation draft provides no self id hint, meaning that 1) conversation
    209                 // self id was not loaded AND 2) the user didn't pick a SIM from the SIM selector.
    210                 // In this case, use the conversation's self id.
    211                 final ConversationListItemData conversation =
    212                         ConversationListItemData.getExistingConversation(db, conversationId);
    213                 if (conversation != null) {
    214                     selfId = conversation.getSelfId();
    215                 } else {
    216                     LogUtil.w(LogUtil.BUGLE_DATAMODEL_TAG, "Conversation " + conversationId +
    217                             "already deleted before sending draft message " +
    218                             message.getMessageId() + ". Aborting InsertNewMessageAction.");
    219                     return null;
    220                 }
    221             }
    222 
    223             // We do not use SubscriptionManager.DEFAULT_SUB_ID for sending a message, so we need
    224             // to bind the message to the system default subscription if it's unbound.
    225             final ParticipantData unboundSelf = BugleDatabaseOperations.getExistingParticipant(
    226                     db, selfId);
    227             if (unboundSelf.getSubId() == ParticipantData.DEFAULT_SELF_SUB_ID
    228                     && OsUtil.isAtLeastL_MR1()) {
    229                 final int defaultSubId = PhoneUtils.getDefault().getDefaultSmsSubscriptionId();
    230                 self = BugleDatabaseOperations.getOrCreateSelf(db, defaultSubId);
    231             } else {
    232                 self = unboundSelf;
    233             }
    234         }
    235         return self;
    236     }
    237 
    238     /** Create MessageData using KEY_RECIPIENTS, KEY_MESSAGE_TEXT and KEY_SUBJECT */
    239     private MessageData createMessage() {
    240         // First find the thread id for this list of participants.
    241         final String recipientsList = actionParameters.getString(KEY_RECIPIENTS);
    242         final String messageText = actionParameters.getString(KEY_MESSAGE_TEXT);
    243         final String subjectText = actionParameters.getString(KEY_SUBJECT_TEXT);
    244         final int subId = actionParameters.getInt(
    245                 KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
    246 
    247         final ArrayList<ParticipantData> participants = new ArrayList<>();
    248         for (final String recipient : recipientsList.split(",")) {
    249             participants.add(ParticipantData.getFromRawPhoneBySimLocale(recipient, subId));
    250         }
    251         if (participants.size() == 0) {
    252             Assert.fail("InsertNewMessage: Empty participants");
    253             return null;
    254         }
    255 
    256         final DatabaseWrapper db = DataModel.get().getDatabase();
    257         BugleDatabaseOperations.sanitizeConversationParticipants(participants);
    258         final ArrayList<String> recipients =
    259                 BugleDatabaseOperations.getRecipientsFromConversationParticipants(participants);
    260         if (recipients.size() == 0) {
    261             Assert.fail("InsertNewMessage: Empty recipients");
    262             return null;
    263         }
    264 
    265         final long threadId = MmsUtils.getOrCreateThreadId(Factory.get().getApplicationContext(),
    266                 recipients);
    267 
    268         if (threadId < 0) {
    269             Assert.fail("InsertNewMessage: Couldn't get threadId in SMS db for these recipients: "
    270                     + recipients.toString());
    271             // TODO: How do we fail the action?
    272             return null;
    273         }
    274 
    275         final String conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
    276                 false, participants, false, false, null);
    277 
    278         final ParticipantData self = BugleDatabaseOperations.getOrCreateSelf(db, subId);
    279 
    280         if (TextUtils.isEmpty(subjectText)) {
    281             return MessageData.createDraftSmsMessage(conversationId, self.getId(), messageText);
    282         } else {
    283             return MessageData.createDraftMmsMessage(conversationId, self.getId(), messageText,
    284                     subjectText);
    285         }
    286     }
    287 
    288     private void insertBroadcastSmsMessage(final String conversationId,
    289             final MessageData message, final int subId, final long laterTimestamp,
    290             final ArrayList<String> recipients) {
    291         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    292             LogUtil.v(TAG, "InsertNewMessageAction: Inserting broadcast SMS message "
    293                     + message.getMessageId());
    294         }
    295         final Context context = Factory.get().getApplicationContext();
    296         final DatabaseWrapper db = DataModel.get().getDatabase();
    297 
    298         // Inform sync that message is being added at timestamp
    299         final SyncManager syncManager = DataModel.get().getSyncManager();
    300         syncManager.onNewMessageInserted(laterTimestamp);
    301 
    302         final long threadId = BugleDatabaseOperations.getThreadId(db, conversationId);
    303         final String address = TextUtils.join(" ", recipients);
    304 
    305         final String messageText = message.getMessageText();
    306         // Insert message into telephony database sms message table
    307         final Uri messageUri = MmsUtils.insertSmsMessage(context,
    308                 Telephony.Sms.CONTENT_URI,
    309                 subId,
    310                 address,
    311                 messageText,
    312                 laterTimestamp,
    313                 Telephony.Sms.STATUS_COMPLETE,
    314                 Telephony.Sms.MESSAGE_TYPE_SENT, threadId);
    315         if (messageUri != null && !TextUtils.isEmpty(messageUri.toString())) {
    316             db.beginTransaction();
    317             try {
    318                 message.updateSendingMessage(conversationId, messageUri, laterTimestamp);
    319                 message.markMessageSent(laterTimestamp);
    320 
    321                 BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
    322 
    323                 BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
    324                         conversationId, message.getMessageId(), laterTimestamp,
    325                         false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
    326                 db.setTransactionSuccessful();
    327             } finally {
    328                 db.endTransaction();
    329             }
    330 
    331             if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    332                 LogUtil.d(TAG, "InsertNewMessageAction: Inserted broadcast SMS message "
    333                         + message.getMessageId() + ", uri = " + message.getSmsMessageUri());
    334             }
    335             MessagingContentProvider.notifyMessagesChanged(conversationId);
    336             MessagingContentProvider.notifyPartsChanged();
    337         } else {
    338             // Ignore error as we only really care about the individual messages?
    339             LogUtil.e(TAG,
    340                     "InsertNewMessageAction: No uri for broadcast SMS " + message.getMessageId()
    341                     + " inserted into telephony DB");
    342         }
    343     }
    344 
    345     /**
    346      * Insert SMS messaging into our database and telephony db.
    347      */
    348     private MessageData insertSendingSmsMessage(final MessageData content, final int subId,
    349             final String recipient, final long timestamp, final String sendingConversationId) {
    350         sLastSentMessageTimestamp = timestamp;
    351 
    352         final Context context = Factory.get().getApplicationContext();
    353 
    354         // Inform sync that message is being added at timestamp
    355         final SyncManager syncManager = DataModel.get().getSyncManager();
    356         syncManager.onNewMessageInserted(timestamp);
    357 
    358         final DatabaseWrapper db = DataModel.get().getDatabase();
    359 
    360         // Send a single message
    361         long threadId;
    362         String conversationId;
    363         if (sendingConversationId == null) {
    364             // For 1:1 message generated sending broadcast need to look up threadId+conversationId
    365             threadId = MmsUtils.getOrCreateSmsThreadId(context, recipient);
    366             conversationId = BugleDatabaseOperations.getOrCreateConversationFromRecipient(
    367                     db, threadId, false /* sender blocked */,
    368                     ParticipantData.getFromRawPhoneBySimLocale(recipient, subId));
    369         } else {
    370             // Otherwise just look up threadId
    371             threadId = BugleDatabaseOperations.getThreadId(db, sendingConversationId);
    372             conversationId = sendingConversationId;
    373         }
    374 
    375         final String messageText = content.getMessageText();
    376 
    377         // Insert message into telephony database sms message table
    378         final Uri messageUri = MmsUtils.insertSmsMessage(context,
    379                 Telephony.Sms.CONTENT_URI,
    380                 subId,
    381                 recipient,
    382                 messageText,
    383                 timestamp,
    384                 Telephony.Sms.STATUS_NONE,
    385                 Telephony.Sms.MESSAGE_TYPE_SENT, threadId);
    386 
    387         MessageData message = null;
    388         if (messageUri != null && !TextUtils.isEmpty(messageUri.toString())) {
    389             db.beginTransaction();
    390             try {
    391                 message = MessageData.createDraftSmsMessage(conversationId,
    392                         content.getSelfId(), messageText);
    393                 message.updateSendingMessage(conversationId, messageUri, timestamp);
    394 
    395                 BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
    396 
    397                 // Do not update the conversation summary to reflect autogenerated 1:1 messages
    398                 if (sendingConversationId != null) {
    399                     BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
    400                             conversationId, message.getMessageId(), timestamp,
    401                             false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
    402                 }
    403                 db.setTransactionSuccessful();
    404             } finally {
    405                 db.endTransaction();
    406             }
    407 
    408             if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    409                 LogUtil.d(TAG, "InsertNewMessageAction: Inserted SMS message "
    410                         + message.getMessageId() + " (uri = " + message.getSmsMessageUri()
    411                         + ", timestamp = " + message.getReceivedTimeStamp() + ")");
    412             }
    413             MessagingContentProvider.notifyMessagesChanged(conversationId);
    414             MessagingContentProvider.notifyPartsChanged();
    415         } else {
    416             LogUtil.e(TAG, "InsertNewMessageAction: No uri for SMS inserted into telephony DB");
    417         }
    418 
    419         return message;
    420     }
    421 
    422     /**
    423      * Insert MMS messaging into our database.
    424      */
    425     private MessageData insertSendingMmsMessage(final String conversationId,
    426             final MessageData message, final long timestamp) {
    427         final DatabaseWrapper db = DataModel.get().getDatabase();
    428         db.beginTransaction();
    429         final List<MessagePartData> attachmentsUpdated = new ArrayList<>();
    430         try {
    431             sLastSentMessageTimestamp = timestamp;
    432 
    433             // Insert "draft" message as placeholder until the final message is written to
    434             // the telephony db
    435             message.updateSendingMessage(conversationId, null/*messageUri*/, timestamp);
    436 
    437             // No need to inform SyncManager as message currently has no Uri...
    438             BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
    439 
    440             BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
    441                     conversationId, message.getMessageId(), timestamp,
    442                     false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
    443 
    444             db.setTransactionSuccessful();
    445         } finally {
    446             db.endTransaction();
    447         }
    448 
    449         if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    450             LogUtil.d(TAG, "InsertNewMessageAction: Inserted MMS message "
    451                     + message.getMessageId() + " (timestamp = " + timestamp + ")");
    452         }
    453         MessagingContentProvider.notifyMessagesChanged(conversationId);
    454         MessagingContentProvider.notifyPartsChanged();
    455 
    456         return message;
    457     }
    458 
    459     private InsertNewMessageAction(final Parcel in) {
    460         super(in);
    461     }
    462 
    463     public static final Parcelable.Creator<InsertNewMessageAction> CREATOR
    464             = new Parcelable.Creator<InsertNewMessageAction>() {
    465         @Override
    466         public InsertNewMessageAction createFromParcel(final Parcel in) {
    467             return new InsertNewMessageAction(in);
    468         }
    469 
    470         @Override
    471         public InsertNewMessageAction[] newArray(final int size) {
    472             return new InsertNewMessageAction[size];
    473         }
    474     };
    475 
    476     @Override
    477     public void writeToParcel(final Parcel parcel, final int flags) {
    478         writeActionToParcel(parcel, flags);
    479     }
    480 }
    481