• 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  * 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  */
52 public 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      */
queueForSendInBackground(final String messageId, final Action processingAction)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      */
SendMessageAction()89     private SendMessageAction() {
90         super();
91     }
92 
93     /**
94      * Read message from database and queue actual sending
95      */
queueAction(final String messageId, final Action processingAction)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
executeAction()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
doBackgroundWork()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 
updateMessageUri(final String messageId, final Uri updatedMessageUri)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
processBackgroundResponse(final Bundle response)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
processBackgroundFailure()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      */
updateMessageAndStatus(final boolean isSms, final MessageData message, final Uri updatedMessageUri, final boolean clearSeen)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 
SendMessageAction(final Parcel in)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
writeToParcel(final Parcel parcel, final int flags)444     public void writeToParcel(final Parcel parcel, final int flags) {
445         writeActionToParcel(parcel, flags);
446     }
447 }
448