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