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