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