1 package com.android.exchange.eas; 2 3 import android.content.ContentResolver; 4 import android.content.ContentUris; 5 import android.content.ContentValues; 6 import android.content.Context; 7 8 import com.android.emailcommon.provider.Account; 9 import com.android.emailcommon.provider.EmailContent; 10 import com.android.emailcommon.provider.MessageMove; 11 import com.android.exchange.EasResponse; 12 import com.android.exchange.adapter.MoveItemsParser; 13 import com.android.exchange.adapter.Serializer; 14 import com.android.exchange.adapter.Tags; 15 import com.android.mail.utils.LogUtils; 16 17 import org.apache.http.HttpEntity; 18 19 import java.io.IOException; 20 import 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 */ 27 public 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