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