1package com.android.emailcommon.provider;
2
3import android.content.ContentResolver;
4import android.content.ContentUris;
5import android.content.Context;
6import android.database.Cursor;
7import android.net.Uri;
8import android.support.v4.util.LongSparseArray;
9
10import com.android.mail.utils.LogUtils;
11
12import java.util.ArrayList;
13import java.util.List;
14
15/**
16 * {@link EmailContent}-like class for the MessageMove table.
17 */
18public class MessageMove extends MessageChangeLogTable {
19    /** Logging tag. */
20    public static final String LOG_TAG = "MessageMove";
21
22    /** The name for this table in the database. */
23    public static final String TABLE_NAME = "MessageMove";
24
25    /** The path for the URI for interacting with message moves. */
26    public static final String PATH = "messageMove";
27
28    /** The URI for dealing with message move data. */
29    public static Uri CONTENT_URI;
30
31    // DB columns.
32    /** Column name for a foreign key into Mailbox for the folder the message is moving from. */
33    public static final String SRC_FOLDER_KEY = "srcFolderKey";
34    /** Column name for a foreign key into Mailbox for the folder the message is moving to. */
35    public static final String DST_FOLDER_KEY = "dstFolderKey";
36    /** Column name for the server-side id for srcFolderKey. */
37    public static final String SRC_FOLDER_SERVER_ID = "srcFolderServerId";
38    /** Column name for the server-side id for dstFolderKey. */
39    public static final String DST_FOLDER_SERVER_ID = "dstFolderServerId";
40
41    /** Selection to get the last synced folder for a message. */
42    private static final String SELECTION_LAST_SYNCED_MAILBOX = MESSAGE_KEY + "=? and " + STATUS
43            + "!=" + STATUS_FAILED_STRING;
44
45    /**
46     * Projection for a query to get all columns necessary for an actual move.
47     */
48    private interface ProjectionMoveQuery {
49        public static final int COLUMN_ID = 0;
50        public static final int COLUMN_MESSAGE_KEY = 1;
51        public static final int COLUMN_SERVER_ID = 2;
52        public static final int COLUMN_SRC_FOLDER_KEY = 3;
53        public static final int COLUMN_DST_FOLDER_KEY = 4;
54        public static final int COLUMN_SRC_FOLDER_SERVER_ID = 5;
55        public static final int COLUMN_DST_FOLDER_SERVER_ID = 6;
56
57        public static final String[] PROJECTION = new String[] {
58                ID, MESSAGE_KEY, SERVER_ID,
59                SRC_FOLDER_KEY, DST_FOLDER_KEY,
60                SRC_FOLDER_SERVER_ID, DST_FOLDER_SERVER_ID
61        };
62    }
63
64    /**
65     * Projection for a query to get the original folder id for a message.
66     */
67    private interface ProjectionLastSyncedMailboxQuery {
68        public static final int COLUMN_ID = 0;
69        public static final int COLUMN_SRC_FOLDER_KEY = 1;
70
71        public static final String[] PROJECTION = new String[] { ID, SRC_FOLDER_KEY };
72    }
73
74    // The actual fields.
75    private final long mSrcFolderKey;
76    private long mDstFolderKey;
77    private final String mSrcFolderServerId;
78    private String mDstFolderServerId;
79
80    private MessageMove(final long messageKey,final String serverId, final long id,
81            final long srcFolderKey, final long dstFolderKey,
82            final String srcFolderServerId, final String dstFolderServerId) {
83        super(messageKey, serverId, id);
84        mSrcFolderKey = srcFolderKey;
85        mDstFolderKey = dstFolderKey;
86        mSrcFolderServerId = srcFolderServerId;
87        mDstFolderServerId = dstFolderServerId;
88    }
89
90    public final long getSourceFolderKey() {
91        return mSrcFolderKey;
92    }
93
94    public final String getSourceFolderId() {
95        return mSrcFolderServerId;
96    }
97
98    public final String getDestFolderId() {
99        return mDstFolderServerId;
100    }
101
102    /**
103     * Initialize static state for this class.
104     */
105    public static void init() {
106        CONTENT_URI = EmailContent.CONTENT_URI.buildUpon().appendEncodedPath(PATH).build();
107    }
108
109    /**
110     * Get the final moves that we want to upsync to the server, setting the status in the DB for
111     * all rows to {@link #STATUS_PROCESSING} that are being updated and to {@link #STATUS_FAILED}
112     * for any old updates.
113     * Messages whose sequence of pending moves results in a no-op (i.e. the message has been moved
114     * back to its original folder) have their moves cleared from the DB without any upsync.
115     * @param context A {@link Context}.
116     * @param accountId The account we want to update.
117     * @return The final moves to send to the server, or null if there are none.
118     */
119    public static List<MessageMove> getMoves(final Context context, final long accountId) {
120        final ContentResolver cr = context.getContentResolver();
121        final Cursor c = getCursor(cr, CONTENT_URI, ProjectionMoveQuery.PROJECTION, accountId);
122        if (c == null) {
123            return null;
124        }
125
126        // Collapse any rows in the cursor that are acting on the same message. We know the cursor
127        // returned by getRowsToProcess is ordered from oldest to newest, and we use this fact to
128        // get the original and final folder for the message.
129        LongSparseArray<MessageMove> movesMap = new LongSparseArray();
130        try {
131            while (c.moveToNext()) {
132                final long id = c.getLong(ProjectionMoveQuery.COLUMN_ID);
133                final long messageKey = c.getLong(ProjectionMoveQuery.COLUMN_MESSAGE_KEY);
134                final String serverId = c.getString(ProjectionMoveQuery.COLUMN_SERVER_ID);
135                final long srcFolderKey = c.getLong(ProjectionMoveQuery.COLUMN_SRC_FOLDER_KEY);
136                final long dstFolderKey = c.getLong(ProjectionMoveQuery.COLUMN_DST_FOLDER_KEY);
137                final String srcFolderServerId =
138                        c.getString(ProjectionMoveQuery.COLUMN_SRC_FOLDER_SERVER_ID);
139                final String dstFolderServerId =
140                        c.getString(ProjectionMoveQuery.COLUMN_DST_FOLDER_SERVER_ID);
141                final MessageMove existingMove = movesMap.get(messageKey);
142                if (existingMove != null) {
143                    if (existingMove.mLastId >= id) {
144                        LogUtils.w(LOG_TAG, "Moves were not in ascending id order");
145                    }
146                    if (!existingMove.mDstFolderServerId.equals(srcFolderServerId) ||
147                            existingMove.mDstFolderKey != srcFolderKey) {
148                        LogUtils.w(LOG_TAG, "existing move's dst not same as this move's src");
149                    }
150                    existingMove.mDstFolderKey = dstFolderKey;
151                    existingMove.mDstFolderServerId = dstFolderServerId;
152                    existingMove.mLastId = id;
153                } else {
154                    movesMap.put(messageKey, new MessageMove(messageKey, serverId, id,
155                            srcFolderKey, dstFolderKey, srcFolderServerId, dstFolderServerId));
156                }
157            }
158        } finally {
159            c.close();
160        }
161
162        // Prune any no-op moves (i.e. messages that have been moved back to the initial folder).
163        final int moveCount = movesMap.size();
164        final long[] unmovedMessages = new long[moveCount];
165        int unmovedMessagesCount = 0;
166        final ArrayList<MessageMove> moves = new ArrayList(moveCount);
167        for (int i = 0; i < movesMap.size(); ++i) {
168            final MessageMove move = movesMap.valueAt(i);
169            // We also treat changes without a server id as a no-op.
170            if ((move.mServerId == null || move.mServerId.length() == 0) ||
171                    move.mSrcFolderKey == move.mDstFolderKey) {
172                unmovedMessages[unmovedMessagesCount] = move.mMessageKey;
173                ++unmovedMessagesCount;
174            } else {
175                moves.add(move);
176            }
177        }
178        if (unmovedMessagesCount != 0) {
179            deleteRowsForMessages(cr, CONTENT_URI, unmovedMessages, unmovedMessagesCount);
180        }
181        if (moves.isEmpty()) {
182            return null;
183        }
184        return moves;
185    }
186
187    /**
188     * Clean up the table to reflect a successful set of upsyncs.
189     * @param cr A {@link ContentResolver}
190     * @param messageKeys The messages to update.
191     * @param count The number of messages.
192     */
193    public static void upsyncSuccessful(final ContentResolver cr, final long[] messageKeys,
194            final int count) {
195        deleteRowsForMessages(cr, CONTENT_URI, messageKeys, count);
196    }
197
198    /**
199     * Clean up the table to reflect upsyncs that need to be retried.
200     * @param cr A {@link ContentResolver}
201     * @param messageKeys The messages to update.
202     * @param count The number of messages.
203     */
204    public static void upsyncRetry(final ContentResolver cr, final long[] messageKeys,
205            final int count) {
206        retryMessages(cr, CONTENT_URI, messageKeys, count);
207    }
208
209    /**
210     * Clean up the table to reflect upsyncs that failed and need to be reverted.
211     * @param cr A {@link ContentResolver}
212     * @param messageKeys The messages to update.
213     * @param count The number of messages.
214     */
215    public static void upsyncFail(final ContentResolver cr, final long[] messageKeys,
216            final int count) {
217        failMessages(cr, CONTENT_URI, messageKeys, count);
218    }
219
220    /**
221     * Get the id for the mailbox this message is in (from the server's point of view).
222     * @param cr A {@link ContentResolver}.
223     * @param messageId The message we're interested in.
224     * @return The id for the mailbox this message was in.
225     */
226    public static long getLastSyncedMailboxForMessage(final ContentResolver cr,
227            final long messageId) {
228        // Check if there's a pending move and get the original mailbox id.
229        final String[] selectionArgs = { String.valueOf(messageId) };
230        final Cursor moveCursor = cr.query(CONTENT_URI, ProjectionLastSyncedMailboxQuery.PROJECTION,
231                SELECTION_LAST_SYNCED_MAILBOX, selectionArgs, ID + " ASC");
232        if (moveCursor != null) {
233            try {
234                if (moveCursor.moveToFirst()) {
235                    // We actually only care about the oldest one, i.e. the one we last got
236                    // from the server before we started mucking with it.
237                    return moveCursor.getLong(
238                            ProjectionLastSyncedMailboxQuery.COLUMN_SRC_FOLDER_KEY);
239                }
240            } finally {
241                moveCursor.close();
242            }
243        }
244
245        // There are no pending moves for this message, so use the one in the Message table.
246        final Cursor messageCursor = cr.query(ContentUris.withAppendedId(
247                EmailContent.Message.CONTENT_URI, messageId),
248                EmailContent.Message.MAILBOX_KEY_PROJECTION, null, null, null);
249        if (messageCursor != null) {
250            try {
251                if (messageCursor.moveToFirst()) {
252                    return messageCursor.getLong(0);
253                }
254            } finally {
255                messageCursor.close();
256            }
257        }
258        return Mailbox.NO_MAILBOX;
259    }
260}
261