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