1package com.android.exchange.eas;
2
3import android.content.ContentResolver;
4import android.content.ContentUris;
5import android.content.ContentValues;
6import android.content.Context;
7
8import com.android.emailcommon.provider.Account;
9import com.android.emailcommon.provider.EmailContent;
10import com.android.emailcommon.provider.MessageMove;
11import com.android.exchange.EasResponse;
12import com.android.exchange.adapter.MoveItemsParser;
13import com.android.exchange.adapter.Serializer;
14import com.android.exchange.adapter.Tags;
15import com.android.mail.utils.LogUtils;
16
17import org.apache.http.HttpEntity;
18
19import java.io.IOException;
20import java.util.List;
21
22/**
23 * Performs a MoveItems request, which is used to move items between collections.
24 * See http://msdn.microsoft.com/en-us/library/ee160102(v=exchg.80).aspx for more details.
25 * TODO: Investigate how this interacts with ItemOperations.
26 */
27public class EasMoveItems extends EasOperation {
28
29    /** Result code indicating that no moved messages were found for this account. */
30    public final static int RESULT_NO_MESSAGES = 0;
31    public final static int RESULT_OK = 1;
32    public final static int RESULT_EMPTY_RESPONSE = 2;
33
34    private static class MoveResponse {
35        public final String sourceMessageId;
36        public final String newMessageId;
37        public final int moveStatus;
38
39        public MoveResponse(final String srcMsgId, final String dstMsgId, final int status) {
40            sourceMessageId = srcMsgId;
41            newMessageId = dstMsgId;
42            moveStatus = status;
43        }
44    }
45
46    private MessageMove mMove;
47    private MoveResponse mResponse;
48
49    public EasMoveItems(final Context context, final Account account) {
50        super(context, account);
51    }
52
53    // TODO: Allow multiple messages in one request. Requires parser changes.
54    public int upsyncMovedMessages() {
55        final List<MessageMove> moves = MessageMove.getMoves(mContext, getAccountId());
56        if (moves == null) {
57            return RESULT_NO_MESSAGES;
58        }
59
60        final long[][] messageIds = new long[3][moves.size()];
61        final int[] counts = new int[3];
62        int result = RESULT_NO_MESSAGES;
63
64        for (final MessageMove move : moves) {
65            mMove = move;
66            if (result >= 0) {
67                // If our previous time through the loop succeeded, keep making server requests.
68                // Otherwise, we carry through the loop for all messages with the last error
69                // response, which will stop trying this iteration and force the rest of the
70                // messages into the retry state.
71                result = performOperation();
72            }
73            final int status;
74            if (result >= 0) {
75                if (result == RESULT_OK) {
76                    processResponse(mMove, mResponse);
77                    status = mResponse.moveStatus;
78                } else {
79                    // TODO: Should this really be a retry?
80                    // We got a 200 response with an empty payload. It's not clear we ought to
81                    // retry, but this is how our implementation has worked in the past.
82                    status = MoveItemsParser.STATUS_CODE_RETRY;
83                }
84            } else {
85                // performOperation returned a negative status code, indicating a failure before the
86                // server actually was able to tell us yea or nay, so we must retry.
87                status = MoveItemsParser.STATUS_CODE_RETRY;
88            }
89            final int index;
90            if (status <= 0) {
91                LogUtils.e(LOG_TAG, "MoveItems gave us an invalid status %d", status);
92                index = MoveItemsParser.STATUS_CODE_RETRY - 1;
93            } else {
94                index = status - 1;
95            }
96            messageIds[index][counts[index]] = mMove.getMessageId();
97            ++counts[index];
98        }
99
100        final ContentResolver cr = mContext.getContentResolver();
101        MessageMove.upsyncSuccessful(cr, messageIds[0], counts[0]);
102        MessageMove.upsyncFail(cr, messageIds[1], counts[1]);
103        MessageMove.upsyncRetry(cr, messageIds[2], counts[2]);
104
105        if (result >= 0) {
106            return RESULT_OK;
107        }
108        return result;
109    }
110
111    @Override
112    protected String getCommand() {
113        return "MoveItems";
114    }
115
116    @Override
117    protected HttpEntity getRequestEntity() throws IOException {
118        final Serializer s = new Serializer();
119        s.start(Tags.MOVE_MOVE_ITEMS);
120        s.start(Tags.MOVE_MOVE);
121        s.data(Tags.MOVE_SRCMSGID, mMove.getServerId());
122        s.data(Tags.MOVE_SRCFLDID, mMove.getSourceFolderId());
123        s.data(Tags.MOVE_DSTFLDID, mMove.getDestFolderId());
124        s.end();
125        s.end().done();
126        return makeEntity(s);
127    }
128
129    @Override
130    protected int handleResponse(final EasResponse response) throws IOException {
131        if (!response.isEmpty()) {
132            final MoveItemsParser parser = new MoveItemsParser(response.getInputStream());
133            parser.parse();
134            final String sourceMessageId = parser.getSourceServerId();
135            final String newMessageId = parser.getNewServerId();
136            final int status = parser.getStatusCode();
137            mResponse = new MoveResponse(sourceMessageId, newMessageId, status);
138            return RESULT_OK;
139        }
140        return RESULT_EMPTY_RESPONSE;
141    }
142
143    private void processResponse(final MessageMove request, final MoveResponse response) {
144        // TODO: Eventually this should use a transaction.
145        // TODO: Improve how the parser reports statuses and how we handle them here.
146
147        final String sourceMessageId;
148
149        if (response.sourceMessageId == null) {
150            // The response didn't contain SrcMsgId, despite it being required.
151            LogUtils.e(LOG_TAG,
152                    "MoveItems response for message %d has no SrcMsgId, using request's server id",
153                    request.getMessageId());
154            sourceMessageId = request.getServerId();
155        } else {
156            sourceMessageId = response.sourceMessageId;
157            if (!sourceMessageId.equals(request.getServerId())) {
158                // TODO: This is bad, but we still need to process the response. Just log for now.
159                LogUtils.e(LOG_TAG,
160                        "MoveItems response for message %d has SrcMsgId != request's server id",
161                        request.getMessageId());
162            }
163        }
164
165        final ContentValues cv = new ContentValues(1);
166        if (response.moveStatus == MoveItemsParser.STATUS_CODE_REVERT) {
167            // Restore the old mailbox id
168            cv.put(EmailContent.MessageColumns.MAILBOX_KEY, request.getSourceFolderKey());
169        } else if (response.moveStatus == MoveItemsParser.STATUS_CODE_SUCCESS) {
170            if (response.newMessageId != null && !response.newMessageId.equals(sourceMessageId)) {
171                cv.put(EmailContent.SyncColumns.SERVER_ID, response.newMessageId);
172            }
173        }
174        if (cv.size() != 0) {
175            mContext.getContentResolver().update(
176                    ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI,
177                            request.getMessageId()), cv, null, null);
178        }
179    }
180}
181