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
17package com.android.messaging.datamodel.action;
18
19import android.content.Context;
20import android.net.Uri;
21import android.os.Parcel;
22import android.os.Parcelable;
23import android.provider.Telephony;
24import android.text.TextUtils;
25
26import com.android.messaging.Factory;
27import com.android.messaging.datamodel.BugleDatabaseOperations;
28import com.android.messaging.datamodel.DataModel;
29import com.android.messaging.datamodel.DatabaseWrapper;
30import com.android.messaging.datamodel.MessagingContentProvider;
31import com.android.messaging.datamodel.SyncManager;
32import com.android.messaging.datamodel.data.ConversationListItemData;
33import com.android.messaging.datamodel.data.MessageData;
34import com.android.messaging.datamodel.data.MessagePartData;
35import com.android.messaging.datamodel.data.ParticipantData;
36import com.android.messaging.sms.MmsUtils;
37import com.android.messaging.util.Assert;
38import com.android.messaging.util.LogUtil;
39import com.android.messaging.util.OsUtil;
40import com.android.messaging.util.PhoneUtils;
41
42import java.util.ArrayList;
43import 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 */
51public 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     */
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     */
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     */
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
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
93    private InsertNewMessageAction(final MessageData message) {
94        this(message, ParticipantData.DEFAULT_SELF_SUB_ID);
95        actionParameters.putParcelable(KEY_MESSAGE, message);
96    }
97
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
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
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
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 */
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
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     */
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     */
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
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
477    public void writeToParcel(final Parcel parcel, final int flags) {
478        writeActionToParcel(parcel, flags);
479    }
480}
481