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