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 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 getSelf( final DatabaseWrapper db, final String conversationId, final MessageData message)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 */ createMessage()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 insertBroadcastSmsMessage(final String conversationId, final MessageData message, final int subId, final long laterTimestamp, final ArrayList<String> recipients)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 */ insertSendingSmsMessage(final MessageData content, final int subId, final String recipient, final long timestamp, final String sendingConversationId)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 */ insertSendingMmsMessage(final String conversationId, final MessageData message, final long timestamp)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 InsertNewMessageAction(final Parcel in)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 writeToParcel(final Parcel parcel, final int flags)477 public void writeToParcel(final Parcel parcel, final int flags) { 478 writeActionToParcel(parcel, flags); 479 } 480 } 481