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.app.Activity;
20import android.content.Context;
21import android.net.Uri;
22import android.os.Bundle;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.telephony.PhoneNumberUtils;
26import android.telephony.SmsManager;
27
28import com.android.messaging.Factory;
29import com.android.messaging.datamodel.BugleDatabaseOperations;
30import com.android.messaging.datamodel.BugleNotifications;
31import com.android.messaging.datamodel.DataModel;
32import com.android.messaging.datamodel.DatabaseWrapper;
33import com.android.messaging.datamodel.MmsFileProvider;
34import com.android.messaging.datamodel.data.MessageData;
35import com.android.messaging.datamodel.data.MessagePartData;
36import com.android.messaging.datamodel.data.ParticipantData;
37import com.android.messaging.mmslib.pdu.SendConf;
38import com.android.messaging.sms.MmsConfig;
39import com.android.messaging.sms.MmsSender;
40import com.android.messaging.sms.MmsUtils;
41import com.android.messaging.util.Assert;
42import com.android.messaging.util.LogUtil;
43
44import java.io.File;
45import java.util.ArrayList;
46
47/**
48* Update message status to reflect success or failure
49* Can also update the message itself if a "final" message is now available from telephony db
50*/
51public class ProcessSentMessageAction extends Action {
52    private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
53
54    // These are always set
55    private static final String KEY_SMS = "is_sms";
56    private static final String KEY_SENT_BY_PLATFORM = "sent_by_platform";
57
58    // These are set when we're processing a message sent by the user. They are null for messages
59    // sent automatically (e.g. a NotifyRespInd/AcknowledgeInd sent in response to a download).
60    private static final String KEY_MESSAGE_ID = "message_id";
61    private static final String KEY_MESSAGE_URI = "message_uri";
62    private static final String KEY_UPDATED_MESSAGE_URI = "updated_message_uri";
63    private static final String KEY_SUB_ID = "sub_id";
64
65    // These are set for messages sent by the platform (L+)
66    public static final String KEY_RESULT_CODE = "result_code";
67    public static final String KEY_HTTP_STATUS_CODE = "http_status_code";
68    private static final String KEY_CONTENT_URI = "content_uri";
69    private static final String KEY_RESPONSE = "response";
70    private static final String KEY_RESPONSE_IMPORTANT = "response_important";
71
72    // These are set for messages we sent ourself (legacy), or which we fast-failed before sending.
73    private static final String KEY_STATUS = "status";
74    private static final String KEY_RAW_STATUS = "raw_status";
75
76    // This is called when MMS lib API returns via PendingIntent
77    public static void processMmsSent(final int resultCode, final Uri messageUri,
78            final Bundle extras) {
79        final ProcessSentMessageAction action = new ProcessSentMessageAction();
80        final Bundle params = action.actionParameters;
81        params.putBoolean(KEY_SMS, false);
82        params.putBoolean(KEY_SENT_BY_PLATFORM, true);
83        params.putString(KEY_MESSAGE_ID, extras.getString(SendMessageAction.EXTRA_MESSAGE_ID));
84        params.putParcelable(KEY_MESSAGE_URI, messageUri);
85        params.putParcelable(KEY_UPDATED_MESSAGE_URI,
86                extras.getParcelable(SendMessageAction.EXTRA_UPDATED_MESSAGE_URI));
87        params.putInt(KEY_SUB_ID,
88                extras.getInt(SendMessageAction.KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID));
89        params.putInt(KEY_RESULT_CODE, resultCode);
90        params.putInt(KEY_HTTP_STATUS_CODE, extras.getInt(SmsManager.EXTRA_MMS_HTTP_STATUS, 0));
91        params.putParcelable(KEY_CONTENT_URI,
92                extras.getParcelable(SendMessageAction.EXTRA_CONTENT_URI));
93        params.putByteArray(KEY_RESPONSE, extras.getByteArray(SmsManager.EXTRA_MMS_DATA));
94        params.putBoolean(KEY_RESPONSE_IMPORTANT,
95                extras.getBoolean(SendMessageAction.EXTRA_RESPONSE_IMPORTANT));
96        action.start();
97    }
98
99    public static void processMessageSentFastFailed(final String messageId,
100            final Uri messageUri, final Uri updatedMessageUri, final int subId, final boolean isSms,
101            final int status, final int rawStatus, final int resultCode) {
102        final ProcessSentMessageAction action = new ProcessSentMessageAction();
103        final Bundle params = action.actionParameters;
104        params.putBoolean(KEY_SMS, isSms);
105        params.putBoolean(KEY_SENT_BY_PLATFORM, false);
106        params.putString(KEY_MESSAGE_ID, messageId);
107        params.putParcelable(KEY_MESSAGE_URI, messageUri);
108        params.putParcelable(KEY_UPDATED_MESSAGE_URI, updatedMessageUri);
109        params.putInt(KEY_SUB_ID, subId);
110        params.putInt(KEY_STATUS, status);
111        params.putInt(KEY_RAW_STATUS, rawStatus);
112        params.putInt(KEY_RESULT_CODE, resultCode);
113        action.start();
114    }
115
116    private ProcessSentMessageAction() {
117        // Callers must use one of the static methods above
118    }
119
120    /**
121    * Update message status to reflect success or failure
122    * Can also update the message itself if a "final" message is now available from telephony db
123    */
124    @Override
125    protected Object executeAction() {
126        final Context context = Factory.get().getApplicationContext();
127        final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
128        final Uri messageUri = actionParameters.getParcelable(KEY_MESSAGE_URI);
129        final Uri updatedMessageUri = actionParameters.getParcelable(KEY_UPDATED_MESSAGE_URI);
130        final boolean isSms = actionParameters.getBoolean(KEY_SMS);
131        final boolean sentByPlatform = actionParameters.getBoolean(KEY_SENT_BY_PLATFORM);
132
133        int status = actionParameters.getInt(KEY_STATUS, MmsUtils.MMS_REQUEST_MANUAL_RETRY);
134        int rawStatus = actionParameters.getInt(KEY_RAW_STATUS,
135                MmsUtils.PDU_HEADER_VALUE_UNDEFINED);
136        final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
137
138        if (sentByPlatform) {
139            // Delete temporary file backing the contentUri passed to MMS service
140            final Uri contentUri = actionParameters.getParcelable(KEY_CONTENT_URI);
141            Assert.isTrue(contentUri != null);
142            final File tempFile = MmsFileProvider.getFile(contentUri);
143            long messageSize = 0;
144            if (tempFile.exists()) {
145                messageSize = tempFile.length();
146                tempFile.delete();
147                if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
148                    LogUtil.v(TAG, "ProcessSentMessageAction: Deleted temp file with outgoing "
149                            + "MMS pdu: " + contentUri);
150                }
151            }
152
153            final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
154            final boolean responseImportant = actionParameters.getBoolean(KEY_RESPONSE_IMPORTANT);
155            if (resultCode == Activity.RESULT_OK) {
156                if (responseImportant) {
157                    // Get the status from the response PDU and update telephony
158                    final byte[] response = actionParameters.getByteArray(KEY_RESPONSE);
159                    final SendConf sendConf = MmsSender.parseSendConf(response, subId);
160                    if (sendConf != null) {
161                        final MmsUtils.StatusPlusUri result =
162                                MmsUtils.updateSentMmsMessageStatus(context, messageUri, sendConf);
163                        status = result.status;
164                        rawStatus = result.rawStatus;
165                    }
166                }
167            } else {
168                String errorMsg = "ProcessSentMessageAction: Platform returned error resultCode: "
169                        + resultCode;
170                final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
171                if (httpStatusCode != 0) {
172                    errorMsg += (", HTTP status code: " + httpStatusCode);
173                }
174                LogUtil.w(TAG, errorMsg);
175                status = MmsSender.getErrorResultStatus(resultCode, httpStatusCode);
176
177                // Check for MMS messages that failed because they exceeded the maximum size,
178                // indicated by an I/O error from the platform.
179                if (resultCode == SmsManager.MMS_ERROR_IO_ERROR) {
180                    if (messageSize > MmsConfig.get(subId).getMaxMessageSize()) {
181                        rawStatus = MessageData.RAW_TELEPHONY_STATUS_MESSAGE_TOO_BIG;
182                    }
183                }
184            }
185        }
186        if (messageId != null) {
187            final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
188            final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
189            processResult(
190                    messageId, updatedMessageUri, status, rawStatus, isSms, this, subId,
191                    resultCode, httpStatusCode);
192        } else {
193            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
194                LogUtil.v(TAG, "ProcessSentMessageAction: No sent message to process (it was "
195                        + "probably a notify response for an MMS download)");
196            }
197        }
198        return null;
199    }
200
201    static void processResult(final String messageId, Uri updatedMessageUri, int status,
202            final int rawStatus, final boolean isSms, final Action processingAction,
203            final int subId, final int resultCode, final int httpStatusCode) {
204        final DatabaseWrapper db = DataModel.get().getDatabase();
205        MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
206        final MessageData originalMessage = message;
207        if (message == null) {
208            LogUtil.w(TAG, "ProcessSentMessageAction: Sent message " + messageId
209                    + " missing from local database");
210            return;
211        }
212        final String conversationId = message.getConversationId();
213        if (updatedMessageUri != null) {
214            // Update message if we have newly written final message in the telephony db
215            final MessageData update = MmsUtils.readSendingMmsMessage(updatedMessageUri,
216                    conversationId, message.getParticipantId(), message.getSelfId());
217            if (update != null) {
218                // Set message Id of final message to that of the existing place holder.
219                update.updateMessageId(message.getMessageId());
220                // Update image sizes.
221                update.updateSizesForImageParts();
222                // Temp attachments are no longer needed
223                for (final MessagePartData part : message.getParts()) {
224                    part.destroySync();
225                }
226                message = update;
227                // processResult will rewrite the complete message as part of update
228            } else {
229                updatedMessageUri = null;
230                status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
231                LogUtil.e(TAG, "ProcessSentMessageAction: Unable to read sending message");
232            }
233        }
234
235        final long timestamp = System.currentTimeMillis();
236        boolean failed;
237        if (status == MmsUtils.MMS_REQUEST_SUCCEEDED) {
238            message.markMessageSent(timestamp);
239            failed = false;
240        } else if (status == MmsUtils.MMS_REQUEST_AUTO_RETRY
241                && message.getInResendWindow(timestamp)) {
242            message.markMessageNotSent(timestamp);
243            message.setRawTelephonyStatus(rawStatus);
244            failed = false;
245        } else {
246            message.markMessageFailed(timestamp);
247            message.setRawTelephonyStatus(rawStatus);
248            message.setMessageSeen(false);
249            failed = true;
250        }
251
252        // We have special handling for when a message to an emergency number fails. In this case,
253        // we notify immediately of any failure (even if we auto-retry), and instruct the user to
254        // try calling the emergency number instead.
255        if (status != MmsUtils.MMS_REQUEST_SUCCEEDED) {
256            final ArrayList<String> recipients =
257                    BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);
258            for (final String recipient : recipients) {
259                if (PhoneNumberUtils.isEmergencyNumber(recipient)) {
260                    BugleNotifications.notifyEmergencySmsFailed(recipient, conversationId);
261                    message.markMessageFailedEmergencyNumber(timestamp);
262                    failed = true;
263                    break;
264                }
265            }
266        }
267
268        // Update the message status and optionally refresh the message with final parts/values.
269        if (SendMessageAction.updateMessageAndStatus(isSms, message, updatedMessageUri, failed)) {
270            // We shouldn't show any notifications if we're not allowed to modify Telephony for
271            // this message.
272            if (failed) {
273                BugleNotifications.update(false, BugleNotifications.UPDATE_ERRORS);
274            }
275            BugleActionToasts.onSendMessageOrManualDownloadActionCompleted(
276                    conversationId, !failed, status, isSms, subId, true/*isSend*/);
277        }
278
279        LogUtil.i(TAG, "ProcessSentMessageAction: Done sending " + (isSms ? "SMS" : "MMS")
280                + " message " + message.getMessageId()
281                + " in conversation " + conversationId
282                + "; status is " + MmsUtils.getRequestStatusDescription(status));
283
284        // Whether we succeeded or failed we will check and maybe schedule some more work
285        ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(
286                status != MmsUtils.MMS_REQUEST_SUCCEEDED, processingAction);
287    }
288
289    private ProcessSentMessageAction(final Parcel in) {
290        super(in);
291    }
292
293    public static final Parcelable.Creator<ProcessSentMessageAction> CREATOR
294            = new Parcelable.Creator<ProcessSentMessageAction>() {
295        @Override
296        public ProcessSentMessageAction createFromParcel(final Parcel in) {
297            return new ProcessSentMessageAction(in);
298        }
299
300        @Override
301        public ProcessSentMessageAction[] newArray(final int size) {
302            return new ProcessSentMessageAction[size];
303        }
304    };
305
306    @Override
307    public void writeToParcel(final Parcel parcel, final int flags) {
308        writeActionToParcel(parcel, flags);
309    }
310}
311