summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/datamodel/action/InsertNewMessageAction.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/messaging/datamodel/action/InsertNewMessageAction.java')
-rw-r--r--src/com/android/messaging/datamodel/action/InsertNewMessageAction.java480
1 files changed, 480 insertions, 0 deletions
diff --git a/src/com/android/messaging/datamodel/action/InsertNewMessageAction.java b/src/com/android/messaging/datamodel/action/InsertNewMessageAction.java
new file mode 100644
index 0000000..2567ca9
--- /dev/null
+++ b/src/com/android/messaging/datamodel/action/InsertNewMessageAction.java
@@ -0,0 +1,480 @@
+/*
+ * 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.Context;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.Telephony;
+import android.text.TextUtils;
+
+import com.android.messaging.Factory;
+import com.android.messaging.datamodel.BugleDatabaseOperations;
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.DatabaseWrapper;
+import com.android.messaging.datamodel.MessagingContentProvider;
+import com.android.messaging.datamodel.SyncManager;
+import com.android.messaging.datamodel.data.ConversationListItemData;
+import com.android.messaging.datamodel.data.MessageData;
+import com.android.messaging.datamodel.data.MessagePartData;
+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 com.android.messaging.util.OsUtil;
+import com.android.messaging.util.PhoneUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Action used to convert a draft message to an outgoing message. Its writes SMS messages to
+ * the telephony db, but {@link SendMessageAction} is responsible for inserting MMS message into
+ * the telephony DB. The latter also does the actual sending of the message in the background.
+ * The latter is also responsible for re-sending a failed message.
+ */
+public class InsertNewMessageAction extends Action implements Parcelable {
+ private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
+
+ private static long sLastSentMessageTimestamp = -1;
+
+ /**
+ * Insert message (no listener)
+ */
+ public static void insertNewMessage(final MessageData message) {
+ final InsertNewMessageAction action = new InsertNewMessageAction(message);
+ action.start();
+ }
+
+ /**
+ * Insert message (no listener) with a given non-default subId.
+ */
+ public static void insertNewMessage(final MessageData message, final int subId) {
+ Assert.isFalse(subId == ParticipantData.DEFAULT_SELF_SUB_ID);
+ final InsertNewMessageAction action = new InsertNewMessageAction(message, subId);
+ action.start();
+ }
+
+ /**
+ * Insert message (no listener)
+ */
+ public static void insertNewMessage(final int subId, final String recipients,
+ final String messageText, final String subject) {
+ final InsertNewMessageAction action = new InsertNewMessageAction(
+ subId, recipients, messageText, subject);
+ action.start();
+ }
+
+ public static long getLastSentMessageTimestamp() {
+ return sLastSentMessageTimestamp;
+ }
+
+ private static final String KEY_SUB_ID = "sub_id";
+ private static final String KEY_MESSAGE = "message";
+ private static final String KEY_RECIPIENTS = "recipients";
+ private static final String KEY_MESSAGE_TEXT = "message_text";
+ private static final String KEY_SUBJECT_TEXT = "subject_text";
+
+ private InsertNewMessageAction(final MessageData message) {
+ this(message, ParticipantData.DEFAULT_SELF_SUB_ID);
+ actionParameters.putParcelable(KEY_MESSAGE, message);
+ }
+
+ private InsertNewMessageAction(final MessageData message, final int subId) {
+ super();
+ actionParameters.putParcelable(KEY_MESSAGE, message);
+ actionParameters.putInt(KEY_SUB_ID, subId);
+ }
+
+ private InsertNewMessageAction(final int subId, final String recipients,
+ final String messageText, final String subject) {
+ super();
+ if (TextUtils.isEmpty(recipients) || TextUtils.isEmpty(messageText)) {
+ Assert.fail("InsertNewMessageAction: Can't have empty recipients or message");
+ }
+ actionParameters.putInt(KEY_SUB_ID, subId);
+ actionParameters.putString(KEY_RECIPIENTS, recipients);
+ actionParameters.putString(KEY_MESSAGE_TEXT, messageText);
+ actionParameters.putString(KEY_SUBJECT_TEXT, subject);
+ }
+
+ /**
+ * Add message to database in pending state and queue actual sending
+ */
+ @Override
+ protected Object executeAction() {
+ LogUtil.i(TAG, "InsertNewMessageAction: inserting new message");
+ MessageData message = actionParameters.getParcelable(KEY_MESSAGE);
+ if (message == null) {
+ LogUtil.i(TAG, "InsertNewMessageAction: Creating MessageData with provided data");
+ message = createMessage();
+ if (message == null) {
+ LogUtil.w(TAG, "InsertNewMessageAction: Could not create MessageData");
+ return null;
+ }
+ }
+ final DatabaseWrapper db = DataModel.get().getDatabase();
+ final String conversationId = message.getConversationId();
+
+ final ParticipantData self = getSelf(db, conversationId, message);
+ if (self == null) {
+ return null;
+ }
+ message.bindSelfId(self.getId());
+ // If the user taps the Send button before the conversation draft is created/loaded by
+ // ReadDraftDataAction (maybe the action service thread was busy), the MessageData may not
+ // have the participant id set. It should be equal to the self id, so we'll use that.
+ if (message.getParticipantId() == null) {
+ message.bindParticipantId(self.getId());
+ }
+
+ final long timestamp = System.currentTimeMillis();
+ final ArrayList<String> recipients =
+ BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);
+ if (recipients.size() < 1) {
+ LogUtil.w(TAG, "InsertNewMessageAction: message recipients is empty");
+ return null;
+ }
+ final int subId = self.getSubId();
+
+ // TODO: Work out whether to send with SMS or MMS (taking into account recipients)?
+ final boolean isSms = (message.getProtocol() == MessageData.PROTOCOL_SMS);
+ if (isSms) {
+ String sendingConversationId = conversationId;
+ if (recipients.size() > 1) {
+ // Broadcast SMS - put message in "fake conversation" before farming out to real 1:1
+ final long laterTimestamp = timestamp + 1;
+ // Send a single message
+ insertBroadcastSmsMessage(conversationId, message, subId,
+ laterTimestamp, recipients);
+
+ sendingConversationId = null;
+ }
+
+ for (final String recipient : recipients) {
+ // Start actual sending
+ insertSendingSmsMessage(message, subId, recipient,
+ timestamp, sendingConversationId);
+ }
+
+ // Can now clear draft from conversation (deleting attachments if necessary)
+ BugleDatabaseOperations.updateDraftMessageData(db, conversationId,
+ null /* message */, BugleDatabaseOperations.UPDATE_MODE_CLEAR_DRAFT);
+ } else {
+ final long timestampRoundedToSecond = 1000 * ((timestamp + 500) / 1000);
+ // Write place holder message directly referencing parts from the draft
+ final MessageData messageToSend = insertSendingMmsMessage(conversationId,
+ message, timestampRoundedToSecond);
+
+ // Can now clear draft from conversation (preserving attachments which are now
+ // referenced by messageToSend)
+ BugleDatabaseOperations.updateDraftMessageData(db, conversationId,
+ messageToSend, BugleDatabaseOperations.UPDATE_MODE_CLEAR_DRAFT);
+ }
+ MessagingContentProvider.notifyConversationListChanged();
+ ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
+
+ return message;
+ }
+
+ private ParticipantData getSelf(
+ final DatabaseWrapper db, final String conversationId, final MessageData message) {
+ ParticipantData self;
+ // Check if we are asked to bind to a non-default subId. This is directly passed in from
+ // the UI thread so that the sub id may be locked as soon as the user clicks on the Send
+ // button.
+ final int requestedSubId = actionParameters.getInt(
+ KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
+ if (requestedSubId != ParticipantData.DEFAULT_SELF_SUB_ID) {
+ self = BugleDatabaseOperations.getOrCreateSelf(db, requestedSubId);
+ } else {
+ String selfId = message.getSelfId();
+ if (selfId == null) {
+ // The conversation draft provides no self id hint, meaning that 1) conversation
+ // self id was not loaded AND 2) the user didn't pick a SIM from the SIM selector.
+ // In this case, use the conversation's self id.
+ final ConversationListItemData conversation =
+ ConversationListItemData.getExistingConversation(db, conversationId);
+ if (conversation != null) {
+ selfId = conversation.getSelfId();
+ } else {
+ LogUtil.w(LogUtil.BUGLE_DATAMODEL_TAG, "Conversation " + conversationId +
+ "already deleted before sending draft message " +
+ message.getMessageId() + ". Aborting InsertNewMessageAction.");
+ return null;
+ }
+ }
+
+ // We do not use SubscriptionManager.DEFAULT_SUB_ID for sending a message, so we need
+ // to bind the message to the system default subscription if it's unbound.
+ final ParticipantData unboundSelf = BugleDatabaseOperations.getExistingParticipant(
+ db, selfId);
+ if (unboundSelf.getSubId() == ParticipantData.DEFAULT_SELF_SUB_ID
+ && OsUtil.isAtLeastL_MR1()) {
+ final int defaultSubId = PhoneUtils.getDefault().getDefaultSmsSubscriptionId();
+ self = BugleDatabaseOperations.getOrCreateSelf(db, defaultSubId);
+ } else {
+ self = unboundSelf;
+ }
+ }
+ return self;
+ }
+
+ /** Create MessageData using KEY_RECIPIENTS, KEY_MESSAGE_TEXT and KEY_SUBJECT */
+ private MessageData createMessage() {
+ // First find the thread id for this list of participants.
+ final String recipientsList = actionParameters.getString(KEY_RECIPIENTS);
+ final String messageText = actionParameters.getString(KEY_MESSAGE_TEXT);
+ final String subjectText = actionParameters.getString(KEY_SUBJECT_TEXT);
+ final int subId = actionParameters.getInt(
+ KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
+
+ final ArrayList<ParticipantData> participants = new ArrayList<>();
+ for (final String recipient : recipientsList.split(",")) {
+ participants.add(ParticipantData.getFromRawPhoneBySimLocale(recipient, subId));
+ }
+ if (participants.size() == 0) {
+ Assert.fail("InsertNewMessage: Empty participants");
+ return null;
+ }
+
+ final DatabaseWrapper db = DataModel.get().getDatabase();
+ BugleDatabaseOperations.sanitizeConversationParticipants(participants);
+ final ArrayList<String> recipients =
+ BugleDatabaseOperations.getRecipientsFromConversationParticipants(participants);
+ if (recipients.size() == 0) {
+ Assert.fail("InsertNewMessage: Empty recipients");
+ return null;
+ }
+
+ final long threadId = MmsUtils.getOrCreateThreadId(Factory.get().getApplicationContext(),
+ recipients);
+
+ if (threadId < 0) {
+ Assert.fail("InsertNewMessage: Couldn't get threadId in SMS db for these recipients: "
+ + recipients.toString());
+ // TODO: How do we fail the action?
+ return null;
+ }
+
+ final String conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
+ false, participants, false, false, null);
+
+ final ParticipantData self = BugleDatabaseOperations.getOrCreateSelf(db, subId);
+
+ if (TextUtils.isEmpty(subjectText)) {
+ return MessageData.createDraftSmsMessage(conversationId, self.getId(), messageText);
+ } else {
+ return MessageData.createDraftMmsMessage(conversationId, self.getId(), messageText,
+ subjectText);
+ }
+ }
+
+ private void insertBroadcastSmsMessage(final String conversationId,
+ final MessageData message, final int subId, final long laterTimestamp,
+ final ArrayList<String> recipients) {
+ if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
+ LogUtil.v(TAG, "InsertNewMessageAction: Inserting broadcast SMS message "
+ + message.getMessageId());
+ }
+ final Context context = Factory.get().getApplicationContext();
+ final DatabaseWrapper db = DataModel.get().getDatabase();
+
+ // Inform sync that message is being added at timestamp
+ final SyncManager syncManager = DataModel.get().getSyncManager();
+ syncManager.onNewMessageInserted(laterTimestamp);
+
+ final long threadId = BugleDatabaseOperations.getThreadId(db, conversationId);
+ final String address = TextUtils.join(" ", recipients);
+
+ final String messageText = message.getMessageText();
+ // Insert message into telephony database sms message table
+ final Uri messageUri = MmsUtils.insertSmsMessage(context,
+ Telephony.Sms.CONTENT_URI,
+ subId,
+ address,
+ messageText,
+ laterTimestamp,
+ Telephony.Sms.STATUS_COMPLETE,
+ Telephony.Sms.MESSAGE_TYPE_SENT, threadId);
+ if (messageUri != null && !TextUtils.isEmpty(messageUri.toString())) {
+ db.beginTransaction();
+ try {
+ message.updateSendingMessage(conversationId, messageUri, laterTimestamp);
+ message.markMessageSent(laterTimestamp);
+
+ BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
+
+ BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
+ conversationId, message.getMessageId(), laterTimestamp,
+ false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
+ LogUtil.d(TAG, "InsertNewMessageAction: Inserted broadcast SMS message "
+ + message.getMessageId() + ", uri = " + message.getSmsMessageUri());
+ }
+ MessagingContentProvider.notifyMessagesChanged(conversationId);
+ MessagingContentProvider.notifyPartsChanged();
+ } else {
+ // Ignore error as we only really care about the individual messages?
+ LogUtil.e(TAG,
+ "InsertNewMessageAction: No uri for broadcast SMS " + message.getMessageId()
+ + " inserted into telephony DB");
+ }
+ }
+
+ /**
+ * Insert SMS messaging into our database and telephony db.
+ */
+ private MessageData insertSendingSmsMessage(final MessageData content, final int subId,
+ final String recipient, final long timestamp, final String sendingConversationId) {
+ sLastSentMessageTimestamp = timestamp;
+
+ final Context context = Factory.get().getApplicationContext();
+
+ // Inform sync that message is being added at timestamp
+ final SyncManager syncManager = DataModel.get().getSyncManager();
+ syncManager.onNewMessageInserted(timestamp);
+
+ final DatabaseWrapper db = DataModel.get().getDatabase();
+
+ // Send a single message
+ long threadId;
+ String conversationId;
+ if (sendingConversationId == null) {
+ // For 1:1 message generated sending broadcast need to look up threadId+conversationId
+ threadId = MmsUtils.getOrCreateSmsThreadId(context, recipient);
+ conversationId = BugleDatabaseOperations.getOrCreateConversationFromRecipient(
+ db, threadId, false /* sender blocked */,
+ ParticipantData.getFromRawPhoneBySimLocale(recipient, subId));
+ } else {
+ // Otherwise just look up threadId
+ threadId = BugleDatabaseOperations.getThreadId(db, sendingConversationId);
+ conversationId = sendingConversationId;
+ }
+
+ final String messageText = content.getMessageText();
+
+ // Insert message into telephony database sms message table
+ final Uri messageUri = MmsUtils.insertSmsMessage(context,
+ Telephony.Sms.CONTENT_URI,
+ subId,
+ recipient,
+ messageText,
+ timestamp,
+ Telephony.Sms.STATUS_NONE,
+ Telephony.Sms.MESSAGE_TYPE_SENT, threadId);
+
+ MessageData message = null;
+ if (messageUri != null && !TextUtils.isEmpty(messageUri.toString())) {
+ db.beginTransaction();
+ try {
+ message = MessageData.createDraftSmsMessage(conversationId,
+ content.getSelfId(), messageText);
+ message.updateSendingMessage(conversationId, messageUri, timestamp);
+
+ BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
+
+ // Do not update the conversation summary to reflect autogenerated 1:1 messages
+ if (sendingConversationId != null) {
+ BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
+ conversationId, message.getMessageId(), timestamp,
+ false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
+ LogUtil.d(TAG, "InsertNewMessageAction: Inserted SMS message "
+ + message.getMessageId() + " (uri = " + message.getSmsMessageUri()
+ + ", timestamp = " + message.getReceivedTimeStamp() + ")");
+ }
+ MessagingContentProvider.notifyMessagesChanged(conversationId);
+ MessagingContentProvider.notifyPartsChanged();
+ } else {
+ LogUtil.e(TAG, "InsertNewMessageAction: No uri for SMS inserted into telephony DB");
+ }
+
+ return message;
+ }
+
+ /**
+ * Insert MMS messaging into our database.
+ */
+ private MessageData insertSendingMmsMessage(final String conversationId,
+ final MessageData message, final long timestamp) {
+ final DatabaseWrapper db = DataModel.get().getDatabase();
+ db.beginTransaction();
+ final List<MessagePartData> attachmentsUpdated = new ArrayList<>();
+ try {
+ sLastSentMessageTimestamp = timestamp;
+
+ // Insert "draft" message as placeholder until the final message is written to
+ // the telephony db
+ message.updateSendingMessage(conversationId, null/*messageUri*/, timestamp);
+
+ // No need to inform SyncManager as message currently has no Uri...
+ BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
+
+ BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
+ conversationId, message.getMessageId(), timestamp,
+ false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
+ LogUtil.d(TAG, "InsertNewMessageAction: Inserted MMS message "
+ + message.getMessageId() + " (timestamp = " + timestamp + ")");
+ }
+ MessagingContentProvider.notifyMessagesChanged(conversationId);
+ MessagingContentProvider.notifyPartsChanged();
+
+ return message;
+ }
+
+ private InsertNewMessageAction(final Parcel in) {
+ super(in);
+ }
+
+ public static final Parcelable.Creator<InsertNewMessageAction> CREATOR
+ = new Parcelable.Creator<InsertNewMessageAction>() {
+ @Override
+ public InsertNewMessageAction createFromParcel(final Parcel in) {
+ return new InsertNewMessageAction(in);
+ }
+
+ @Override
+ public InsertNewMessageAction[] newArray(final int size) {
+ return new InsertNewMessageAction[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(final Parcel parcel, final int flags) {
+ writeActionToParcel(parcel, flags);
+ }
+}