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.ContentValues;
20import android.content.Context;
21import android.net.Uri;
22import android.os.Bundle;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.provider.Telephony.Mms;
26import android.provider.Telephony.Sms;
27
28import com.android.messaging.Factory;
29import com.android.messaging.datamodel.BugleDatabaseOperations;
30import com.android.messaging.datamodel.DataModel;
31import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
32import com.android.messaging.datamodel.DatabaseWrapper;
33import com.android.messaging.datamodel.MessagingContentProvider;
34import com.android.messaging.datamodel.SyncManager;
35import com.android.messaging.datamodel.data.MessageData;
36import com.android.messaging.datamodel.data.ParticipantData;
37import com.android.messaging.sms.MmsUtils;
38import com.android.messaging.util.Assert;
39import com.android.messaging.util.LogUtil;
40
41import java.util.ArrayList;
42
43/**
44 * Action used to send an outgoing message. It writes MMS messages to the telephony db
45 * ({@link InsertNewMessageAction}) writes SMS messages to the telephony db). It also
46 * initiates the actual sending. It will all be used for re-sending a failed message.
47 * NOTE: This action must queue a ProcessPendingMessagesAction when it is done (success or failure).
48 * <p>
49 * This class is public (not package-private) because the SMS/MMS (e.g. MmsUtils) classes need to
50 * access the EXTRA_* fields for setting up the 'sent' pending intent.
51 */
52public class SendMessageAction extends Action implements Parcelable {
53    private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
54
55    /**
56     * Queue sending of existing message (can only be called during execute of action)
57     */
58    static boolean queueForSendInBackground(final String messageId,
59            final Action processingAction) {
60        final SendMessageAction action = new SendMessageAction();
61        return action.queueAction(messageId, processingAction);
62    }
63
64    public static final boolean DEFAULT_DELIVERY_REPORT_MODE  = false;
65    public static final int MAX_SMS_RETRY = 3;
66
67    // Core parameters needed for all types of message
68    private static final String KEY_MESSAGE_ID = "message_id";
69    private static final String KEY_MESSAGE = "message";
70    private static final String KEY_MESSAGE_URI = "message_uri";
71    private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number";
72
73    // For sms messages a few extra values are included in the bundle
74    private static final String KEY_RECIPIENT = "recipient";
75    private static final String KEY_RECIPIENTS = "recipients";
76    private static final String KEY_SMS_SERVICE_CENTER = "sms_service_center";
77
78    // Values we attach to the pending intent that's fired when the message is sent.
79    // Only applicable when sending via the platform APIs on L+.
80    public static final String KEY_SUB_ID = "sub_id";
81    public static final String EXTRA_MESSAGE_ID = "message_id";
82    public static final String EXTRA_UPDATED_MESSAGE_URI = "updated_message_uri";
83    public static final String EXTRA_CONTENT_URI = "content_uri";
84    public static final String EXTRA_RESPONSE_IMPORTANT = "response_important";
85
86    /**
87     * Constructor used for retrying sending in the background (only message id available)
88     */
89    private SendMessageAction() {
90        super();
91    }
92
93    /**
94     * Read message from database and queue actual sending
95     */
96    private boolean queueAction(final String messageId, final Action processingAction) {
97        actionParameters.putString(KEY_MESSAGE_ID, messageId);
98
99        final long timestamp = System.currentTimeMillis();
100        final DatabaseWrapper db = DataModel.get().getDatabase();
101
102        final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
103        // Check message can be resent
104        if (message != null && message.canSendMessage()) {
105            final boolean isSms = (message.getProtocol() == MessageData.PROTOCOL_SMS);
106
107            final ParticipantData self = BugleDatabaseOperations.getExistingParticipant(
108                    db, message.getSelfId());
109            final Uri messageUri = message.getSmsMessageUri();
110            final String conversationId = message.getConversationId();
111
112            // Update message status
113            if (message.getYetToSend()) {
114                // Initial sending of message
115                message.markMessageSending(timestamp);
116            } else {
117                // Automatic resend of message
118                message.markMessageResending(timestamp);
119            }
120            if (!updateMessageAndStatus(isSms, message, null /* messageUri */, false /*notify*/)) {
121                // If message is missing in the telephony database we don't need to send it
122                return false;
123            }
124
125            final ArrayList<String> recipients =
126                    BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);
127
128            // Update action state with parameters needed for background sending
129            actionParameters.putParcelable(KEY_MESSAGE_URI, messageUri);
130            actionParameters.putParcelable(KEY_MESSAGE, message);
131            actionParameters.putStringArrayList(KEY_RECIPIENTS, recipients);
132            actionParameters.putInt(KEY_SUB_ID, self.getSubId());
133            actionParameters.putString(KEY_SUB_PHONE_NUMBER, self.getNormalizedDestination());
134
135            if (isSms) {
136                final String smsc = BugleDatabaseOperations.getSmsServiceCenterForConversation(
137                        db, conversationId);
138                actionParameters.putString(KEY_SMS_SERVICE_CENTER, smsc);
139
140                if (recipients.size() == 1) {
141                    final String recipient = recipients.get(0);
142
143                    actionParameters.putString(KEY_RECIPIENT, recipient);
144                    // Queue actual sending for SMS
145                    processingAction.requestBackgroundWork(this);
146
147                    if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
148                        LogUtil.d(TAG, "SendMessageAction: Queued SMS message " + messageId
149                                + " for sending");
150                    }
151                    return true;
152                } else {
153                    LogUtil.wtf(TAG, "Trying to resend a broadcast SMS - not allowed");
154                }
155            } else {
156                // Queue actual sending for MMS
157                processingAction.requestBackgroundWork(this);
158
159                if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
160                    LogUtil.d(TAG, "SendMessageAction: Queued MMS message " + messageId
161                            + " for sending");
162                }
163                return true;
164            }
165        }
166
167        return false;
168    }
169
170
171    /**
172     * Never called
173     */
174    @Override
175    protected Object executeAction() {
176        Assert.fail("SendMessageAction must be queued rather than started");
177        return null;
178    }
179
180    /**
181     * Send message on background worker thread
182     */
183    @Override
184    protected Bundle doBackgroundWork() {
185        final MessageData message = actionParameters.getParcelable(KEY_MESSAGE);
186        final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
187        Uri messageUri = actionParameters.getParcelable(KEY_MESSAGE_URI);
188        Uri updatedMessageUri = null;
189        final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS;
190        final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
191        final String subPhoneNumber = actionParameters.getString(KEY_SUB_PHONE_NUMBER);
192
193        LogUtil.i(TAG, "SendMessageAction: Sending " + (isSms ? "SMS" : "MMS") + " message "
194                + messageId + " in conversation " + message.getConversationId());
195
196        int status;
197        int rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED;
198        int resultCode = MessageData.UNKNOWN_RESULT_CODE;
199        if (isSms) {
200            Assert.notNull(messageUri);
201            final String recipient = actionParameters.getString(KEY_RECIPIENT);
202            final String messageText = message.getMessageText();
203            final String smsServiceCenter = actionParameters.getString(KEY_SMS_SERVICE_CENTER);
204            final boolean deliveryReportRequired = MmsUtils.isDeliveryReportRequired(subId);
205
206            status = MmsUtils.sendSmsMessage(recipient, messageText, messageUri, subId,
207                    smsServiceCenter, deliveryReportRequired);
208        } else {
209            final Context context = Factory.get().getApplicationContext();
210            final ArrayList<String> recipients =
211                    actionParameters.getStringArrayList(KEY_RECIPIENTS);
212            if (messageUri == null) {
213                final long timestamp = message.getReceivedTimeStamp();
214
215                // Inform sync that message has been added at local received timestamp
216                final SyncManager syncManager = DataModel.get().getSyncManager();
217                syncManager.onNewMessageInserted(timestamp);
218
219                // For MMS messages first need to write to telephony (resizing images if needed)
220                updatedMessageUri = MmsUtils.insertSendingMmsMessage(context, recipients,
221                        message, subId, subPhoneNumber, timestamp);
222                if (updatedMessageUri != null) {
223                    messageUri = updatedMessageUri;
224                    // To prevent Sync seeing inconsistent state must write to DB on this thread
225                    updateMessageUri(messageId, updatedMessageUri);
226
227                    if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
228                        LogUtil.v(TAG, "SendMessageAction: Updated message " + messageId
229                                + " with new uri " + messageUri);
230                    }
231                 }
232            }
233            if (messageUri != null) {
234                // Actually send the MMS
235                final Bundle extras = new Bundle();
236                extras.putString(EXTRA_MESSAGE_ID, messageId);
237                extras.putParcelable(EXTRA_UPDATED_MESSAGE_URI, updatedMessageUri);
238                final MmsUtils.StatusPlusUri result = MmsUtils.sendMmsMessage(context, subId,
239                        messageUri, extras);
240                if (result == MmsUtils.STATUS_PENDING) {
241                    // Async send, so no status yet
242                    LogUtil.d(TAG, "SendMessageAction: Sending MMS message " + messageId
243                            + " asynchronously; waiting for callback to finish processing");
244                    return null;
245                }
246                status = result.status;
247                rawStatus = result.rawStatus;
248                resultCode = result.resultCode;
249            } else {
250                status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
251            }
252        }
253
254        // When we fast-fail before calling the MMS lib APIs (e.g. airplane mode,
255        // sending message is deleted).
256        ProcessSentMessageAction.processMessageSentFastFailed(messageId, messageUri,
257                updatedMessageUri, subId, isSms, status, rawStatus, resultCode);
258        return null;
259    }
260
261    private void updateMessageUri(final String messageId, final Uri updatedMessageUri) {
262        final DatabaseWrapper db = DataModel.get().getDatabase();
263        db.beginTransaction();
264        try {
265            final ContentValues values = new ContentValues();
266            values.put(MessageColumns.SMS_MESSAGE_URI, updatedMessageUri.toString());
267            BugleDatabaseOperations.updateMessageRow(db, messageId, values);
268            db.setTransactionSuccessful();
269        } finally {
270            db.endTransaction();
271        }
272    }
273
274    @Override
275    protected Object processBackgroundResponse(final Bundle response) {
276        // Nothing to do here, post-send tasks handled by ProcessSentMessageAction
277        return null;
278    }
279
280    /**
281     * Update message status to reflect success or failure
282     */
283    @Override
284    protected Object processBackgroundFailure() {
285        final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
286        final MessageData message = actionParameters.getParcelable(KEY_MESSAGE);
287        final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS;
288        final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
289        final int resultCode = actionParameters.getInt(ProcessSentMessageAction.KEY_RESULT_CODE);
290        final int httpStatusCode =
291                actionParameters.getInt(ProcessSentMessageAction.KEY_HTTP_STATUS_CODE);
292
293        ProcessSentMessageAction.processResult(messageId, null /* updatedMessageUri */,
294                MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED,
295                isSms, this, subId, resultCode, httpStatusCode);
296
297        // Whether we succeeded or failed we will check and maybe schedule some more work
298        ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(true, this);
299
300        return null;
301    }
302
303    /**
304     * Update the message status (and message itself if necessary)
305     * @param isSms whether this is an SMS or MMS
306     * @param message message to update
307     * @param updatedMessageUri message uri for newly-inserted messages; null otherwise
308     * @param clearSeen whether the message 'seen' status should be reset if error occurs
309     */
310    public static boolean updateMessageAndStatus(final boolean isSms, final MessageData message,
311            final Uri updatedMessageUri, final boolean clearSeen) {
312        final Context context = Factory.get().getApplicationContext();
313        final DatabaseWrapper db = DataModel.get().getDatabase();
314
315        // TODO: We're optimistically setting the type/box of outgoing messages to
316        // 'SENT' even before they actually are. We should technically be using QUEUED or OUTBOX
317        // instead, but if we do that, it's possible that the Messaging app will try to send them
318        // as part of its clean-up logic that runs when it starts (http://b/18155366).
319        //
320        // We also use the wrong status when inserting queued SMS messages in
321        // InsertNewMessageAction.insertBroadcastSmsMessage and insertSendingSmsMessage (should be
322        // QUEUED or OUTBOX), and in MmsUtils.insertSendReq (should be OUTBOX).
323
324        boolean updatedTelephony = true;
325        int messageBox;
326        int type;
327        switch(message.getStatus()) {
328            case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE:
329            case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED:
330                type = Sms.MESSAGE_TYPE_SENT;
331                messageBox = Mms.MESSAGE_BOX_SENT;
332                break;
333            case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND:
334            case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
335                type = Sms.MESSAGE_TYPE_SENT;
336                messageBox = Mms.MESSAGE_BOX_SENT;
337                break;
338            case MessageData.BUGLE_STATUS_OUTGOING_SENDING:
339            case MessageData.BUGLE_STATUS_OUTGOING_RESENDING:
340                type = Sms.MESSAGE_TYPE_SENT;
341                messageBox = Mms.MESSAGE_BOX_SENT;
342                break;
343            case MessageData.BUGLE_STATUS_OUTGOING_FAILED:
344            case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
345                type = Sms.MESSAGE_TYPE_FAILED;
346                messageBox = Mms.MESSAGE_BOX_FAILED;
347                break;
348            default:
349                type = Sms.MESSAGE_TYPE_ALL;
350                messageBox = Mms.MESSAGE_BOX_ALL;
351                break;
352        }
353        // First in the telephony DB
354        if (isSms) {
355            // Ignore update message Uri
356            if (type != Sms.MESSAGE_TYPE_ALL) {
357                if (!MmsUtils.updateSmsMessageSendingStatus(context, message.getSmsMessageUri(),
358                        type, message.getReceivedTimeStamp())) {
359                    message.markMessageFailed(message.getSentTimeStamp());
360                    updatedTelephony = false;
361                }
362            }
363        } else if (message.getSmsMessageUri() != null) {
364            if (messageBox != Mms.MESSAGE_BOX_ALL) {
365                if (!MmsUtils.updateMmsMessageSendingStatus(context, message.getSmsMessageUri(),
366                        messageBox, message.getReceivedTimeStamp())) {
367                    message.markMessageFailed(message.getSentTimeStamp());
368                    updatedTelephony = false;
369                }
370            }
371        }
372        if (updatedTelephony) {
373            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
374                LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS")
375                        + " message " + message.getMessageId()
376                        + " in telephony (" + message.getSmsMessageUri() + ")");
377            }
378        } else {
379            LogUtil.w(TAG, "SendMessageAction: Failed to update " + (isSms ? "SMS" : "MMS")
380                    + " message " + message.getMessageId()
381                    + " in telephony (" + message.getSmsMessageUri() + "); marking message failed");
382        }
383
384        // Update the local DB
385        db.beginTransaction();
386        try {
387            if (updatedMessageUri != null) {
388                // Update all message and part fields
389                BugleDatabaseOperations.updateMessageInTransaction(db, message);
390                BugleDatabaseOperations.refreshConversationMetadataInTransaction(
391                        db, message.getConversationId(), false/* shouldAutoSwitchSelfId */,
392                        false/*archived*/);
393            } else {
394                final ContentValues values = new ContentValues();
395                values.put(MessageColumns.STATUS, message.getStatus());
396
397                if (clearSeen) {
398                    // When a message fails to send, the message needs to
399                    // be unseen to be selected as an error notification.
400                    values.put(MessageColumns.SEEN, 0);
401                }
402                values.put(MessageColumns.RECEIVED_TIMESTAMP, message.getReceivedTimeStamp());
403                values.put(MessageColumns.RAW_TELEPHONY_STATUS, message.getRawTelephonyStatus());
404
405                BugleDatabaseOperations.updateMessageRowIfExists(db, message.getMessageId(),
406                        values);
407            }
408            db.setTransactionSuccessful();
409            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
410                LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS")
411                        + " message " + message.getMessageId() + " in local db. Timestamp = "
412                        + message.getReceivedTimeStamp());
413            }
414        } finally {
415            db.endTransaction();
416        }
417
418        MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
419        if (updatedMessageUri != null) {
420            MessagingContentProvider.notifyPartsChanged();
421        }
422
423        return updatedTelephony;
424    }
425
426    private SendMessageAction(final Parcel in) {
427        super(in);
428    }
429
430    public static final Parcelable.Creator<SendMessageAction> CREATOR
431            = new Parcelable.Creator<SendMessageAction>() {
432        @Override
433        public SendMessageAction createFromParcel(final Parcel in) {
434            return new SendMessageAction(in);
435        }
436
437        @Override
438        public SendMessageAction[] newArray(final int size) {
439            return new SendMessageAction[size];
440        }
441    };
442
443    @Override
444    public void writeToParcel(final Parcel parcel, final int flags) {
445        writeActionToParcel(parcel, flags);
446    }
447}
448