summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/datamodel/action/ProcessSentMessageAction.java
blob: f408e470bfd8e202c9346ee49f4479442b625cbb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.messaging.datamodel.action;

import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.telephony.PhoneNumberUtils;
import android.telephony.SmsManager;

import com.android.messaging.Factory;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.BugleNotifications;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MmsFileProvider;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.MessagePartData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.mmslib.pdu.SendConf;
import com.android.messaging.sms.MmsConfig;
import com.android.messaging.sms.MmsSender;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;

import java.io.File;
import java.util.ArrayList;

/**
* Update message status to reflect success or failure
* Can also update the message itself if a "final" message is now available from telephony db
*/
public class ProcessSentMessageAction extends Action {
    private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;

    // These are always set
    private static final String KEY_SMS = "is_sms";
    private static final String KEY_SENT_BY_PLATFORM = "sent_by_platform";

    // These are set when we're processing a message sent by the user. They are null for messages
    // sent automatically (e.g. a NotifyRespInd/AcknowledgeInd sent in response to a download).
    private static final String KEY_MESSAGE_ID = "message_id";
    private static final String KEY_MESSAGE_URI = "message_uri";
    private static final String KEY_UPDATED_MESSAGE_URI = "updated_message_uri";
    private static final String KEY_SUB_ID = "sub_id";

    // These are set for messages sent by the platform (L+)
    public static final String KEY_RESULT_CODE = "result_code";
    public static final String KEY_HTTP_STATUS_CODE = "http_status_code";
    private static final String KEY_CONTENT_URI = "content_uri";
    private static final String KEY_RESPONSE = "response";
    private static final String KEY_RESPONSE_IMPORTANT = "response_important";

    // These are set for messages we sent ourself (legacy), or which we fast-failed before sending.
    private static final String KEY_STATUS = "status";
    private static final String KEY_RAW_STATUS = "raw_status";

    // This is called when MMS lib API returns via PendingIntent
    public static void processMmsSent(final int resultCode, final Uri messageUri,
            final Bundle extras) {
        final ProcessSentMessageAction action = new ProcessSentMessageAction();
        final Bundle params = action.actionParameters;
        params.putBoolean(KEY_SMS, false);
        params.putBoolean(KEY_SENT_BY_PLATFORM, true);
        params.putString(KEY_MESSAGE_ID, extras.getString(SendMessageAction.EXTRA_MESSAGE_ID));
        params.putParcelable(KEY_MESSAGE_URI, messageUri);
        params.putParcelable(KEY_UPDATED_MESSAGE_URI,
                extras.getParcelable(SendMessageAction.EXTRA_UPDATED_MESSAGE_URI));
        params.putInt(KEY_SUB_ID,
                extras.getInt(SendMessageAction.KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID));
        params.putInt(KEY_RESULT_CODE, resultCode);
        params.putInt(KEY_HTTP_STATUS_CODE, extras.getInt(SmsManager.EXTRA_MMS_HTTP_STATUS, 0));
        params.putParcelable(KEY_CONTENT_URI,
                extras.getParcelable(SendMessageAction.EXTRA_CONTENT_URI));
        params.putByteArray(KEY_RESPONSE, extras.getByteArray(SmsManager.EXTRA_MMS_DATA));
        params.putBoolean(KEY_RESPONSE_IMPORTANT,
                extras.getBoolean(SendMessageAction.EXTRA_RESPONSE_IMPORTANT));
        action.start();
    }

    public static void processMessageSentFastFailed(final String messageId,
            final Uri messageUri, final Uri updatedMessageUri, final int subId, final boolean isSms,
            final int status, final int rawStatus, final int resultCode) {
        final ProcessSentMessageAction action = new ProcessSentMessageAction();
        final Bundle params = action.actionParameters;
        params.putBoolean(KEY_SMS, isSms);
        params.putBoolean(KEY_SENT_BY_PLATFORM, false);
        params.putString(KEY_MESSAGE_ID, messageId);
        params.putParcelable(KEY_MESSAGE_URI, messageUri);
        params.putParcelable(KEY_UPDATED_MESSAGE_URI, updatedMessageUri);
        params.putInt(KEY_SUB_ID, subId);
        params.putInt(KEY_STATUS, status);
        params.putInt(KEY_RAW_STATUS, rawStatus);
        params.putInt(KEY_RESULT_CODE, resultCode);
        action.start();
    }

    private ProcessSentMessageAction() {
        // Callers must use one of the static methods above
    }

    /**
    * Update message status to reflect success or failure
    * Can also update the message itself if a "final" message is now available from telephony db
    */
    @Override
    protected Object executeAction() {
        final Context context = Factory.get().getApplicationContext();
        final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
        final Uri messageUri = actionParameters.getParcelable(KEY_MESSAGE_URI);
        final Uri updatedMessageUri = actionParameters.getParcelable(KEY_UPDATED_MESSAGE_URI);
        final boolean isSms = actionParameters.getBoolean(KEY_SMS);
        final boolean sentByPlatform = actionParameters.getBoolean(KEY_SENT_BY_PLATFORM);

        int status = actionParameters.getInt(KEY_STATUS, MmsUtils.MMS_REQUEST_MANUAL_RETRY);
        int rawStatus = actionParameters.getInt(KEY_RAW_STATUS,
                MmsUtils.PDU_HEADER_VALUE_UNDEFINED);
        final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);

        if (sentByPlatform) {
            // Delete temporary file backing the contentUri passed to MMS service
            final Uri contentUri = actionParameters.getParcelable(KEY_CONTENT_URI);
            Assert.isTrue(contentUri != null);
            final File tempFile = MmsFileProvider.getFile(contentUri);
            long messageSize = 0;
            if (tempFile.exists()) {
                messageSize = tempFile.length();
                tempFile.delete();
                if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                    LogUtil.v(TAG, "ProcessSentMessageAction: Deleted temp file with outgoing "
                            + "MMS pdu: " + contentUri);
                }
            }

            final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
            final boolean responseImportant = actionParameters.getBoolean(KEY_RESPONSE_IMPORTANT);
            if (resultCode == Activity.RESULT_OK) {
                if (responseImportant) {
                    // Get the status from the response PDU and update telephony
                    final byte[] response = actionParameters.getByteArray(KEY_RESPONSE);
                    final SendConf sendConf = MmsSender.parseSendConf(response, subId);
                    if (sendConf != null) {
                        final MmsUtils.StatusPlusUri result =
                                MmsUtils.updateSentMmsMessageStatus(context, messageUri, sendConf);
                        status = result.status;
                        rawStatus = result.rawStatus;
                    }
                }
            } else {
                String errorMsg = "ProcessSentMessageAction: Platform returned error resultCode: "
                        + resultCode;
                final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
                if (httpStatusCode != 0) {
                    errorMsg += (", HTTP status code: " + httpStatusCode);
                }
                LogUtil.w(TAG, errorMsg);
                status = MmsSender.getErrorResultStatus(resultCode, httpStatusCode);

                // Check for MMS messages that failed because they exceeded the maximum size,
                // indicated by an I/O error from the platform.
                if (resultCode == SmsManager.MMS_ERROR_IO_ERROR) {
                    if (messageSize > MmsConfig.get(subId).getMaxMessageSize()) {
                        rawStatus = MessageData.RAW_TELEPHONY_STATUS_MESSAGE_TOO_BIG;
                    }
                }
            }
        }
        if (messageId != null) {
            final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
            final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
            processResult(
                    messageId, updatedMessageUri, status, rawStatus, isSms, this, subId,
                    resultCode, httpStatusCode);
        } else {
            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                LogUtil.v(TAG, "ProcessSentMessageAction: No sent message to process (it was "
                        + "probably a notify response for an MMS download)");
            }
        }
        return null;
    }

    static void processResult(final String messageId, Uri updatedMessageUri, int status,
            final int rawStatus, final boolean isSms, final Action processingAction,
            final int subId, final int resultCode, final int httpStatusCode) {
        final DatabaseWrapper db = DataModel.get().getDatabase();
        MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
        final MessageData originalMessage = message;
        if (message == null) {
            LogUtil.w(TAG, "ProcessSentMessageAction: Sent message " + messageId
                    + " missing from local database");
            return;
        }
        final String conversationId = message.getConversationId();
        if (updatedMessageUri != null) {
            // Update message if we have newly written final message in the telephony db
            final MessageData update = MmsUtils.readSendingMmsMessage(updatedMessageUri,
                    conversationId, message.getParticipantId(), message.getSelfId());
            if (update != null) {
                // Set message Id of final message to that of the existing place holder.
                update.updateMessageId(message.getMessageId());
                // Update image sizes.
                update.updateSizesForImageParts();
                // Temp attachments are no longer needed
                for (final MessagePartData part : message.getParts()) {
                    part.destroySync();
                }
                message = update;
                // processResult will rewrite the complete message as part of update
            } else {
                updatedMessageUri = null;
                status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
                LogUtil.e(TAG, "ProcessSentMessageAction: Unable to read sending message");
            }
        }

        final long timestamp = System.currentTimeMillis();
        boolean failed;
        if (status == MmsUtils.MMS_REQUEST_SUCCEEDED) {
            message.markMessageSent(timestamp);
            failed = false;
        } else if (status == MmsUtils.MMS_REQUEST_AUTO_RETRY
                && message.getInResendWindow(timestamp)) {
            message.markMessageNotSent(timestamp);
            message.setRawTelephonyStatus(rawStatus);
            failed = false;
        } else {
            message.markMessageFailed(timestamp);
            message.setRawTelephonyStatus(rawStatus);
            message.setMessageSeen(false);
            failed = true;
        }

        // We have special handling for when a message to an emergency number fails. In this case,
        // we notify immediately of any failure (even if we auto-retry), and instruct the user to
        // try calling the emergency number instead.
        if (status != MmsUtils.MMS_REQUEST_SUCCEEDED) {
            final ArrayList<String> recipients =
                    BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);
            for (final String recipient : recipients) {
                if (PhoneNumberUtils.isEmergencyNumber(recipient)) {
                    BugleNotifications.notifyEmergencySmsFailed(recipient, conversationId);
                    message.markMessageFailedEmergencyNumber(timestamp);
                    failed = true;
                    break;
                }
            }
        }

        // Update the message status and optionally refresh the message with final parts/values.
        if (SendMessageAction.updateMessageAndStatus(isSms, message, updatedMessageUri, failed)) {
            // We shouldn't show any notifications if we're not allowed to modify Telephony for
            // this message.
            if (failed) {
                BugleNotifications.update(false, BugleNotifications.UPDATE_ERRORS);
            }
            BugleActionToasts.onSendMessageOrManualDownloadActionCompleted(
                    conversationId, !failed, status, isSms, subId, true/*isSend*/);
        }

        LogUtil.i(TAG, "ProcessSentMessageAction: Done sending " + (isSms ? "SMS" : "MMS")
                + " message " + message.getMessageId()
                + " in conversation " + conversationId
                + "; status is " + MmsUtils.getRequestStatusDescription(status));

        // Whether we succeeded or failed we will check and maybe schedule some more work
        ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(
                status != MmsUtils.MMS_REQUEST_SUCCEEDED, processingAction);
    }

    private ProcessSentMessageAction(final Parcel in) {
        super(in);
    }

    public static final Parcelable.Creator<ProcessSentMessageAction> CREATOR
            = new Parcelable.Creator<ProcessSentMessageAction>() {
        @Override
        public ProcessSentMessageAction createFromParcel(final Parcel in) {
            return new ProcessSentMessageAction(in);
        }

        @Override
        public ProcessSentMessageAction[] newArray(final int size) {
            return new ProcessSentMessageAction[size];
        }
    };

    @Override
    public void writeToParcel(final Parcel parcel, final int flags) {
        writeActionToParcel(parcel, flags);
    }
}