package com.android.emailcommon.provider; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.support.v4.util.LongSparseArray; import com.android.mail.utils.LogUtils; import java.util.ArrayList; import java.util.List; /** * {@link EmailContent}-like class for the MessageMove table. */ public class MessageMove extends MessageChangeLogTable { /** Logging tag. */ public static final String LOG_TAG = "MessageMove"; /** The name for this table in the database. */ public static final String TABLE_NAME = "MessageMove"; /** The path for the URI for interacting with message moves. */ public static final String PATH = "messageMove"; /** The URI for dealing with message move data. */ public static Uri CONTENT_URI; // DB columns. /** Column name for a foreign key into Mailbox for the folder the message is moving from. */ public static final String SRC_FOLDER_KEY = "srcFolderKey"; /** Column name for a foreign key into Mailbox for the folder the message is moving to. */ public static final String DST_FOLDER_KEY = "dstFolderKey"; /** Column name for the server-side id for srcFolderKey. */ public static final String SRC_FOLDER_SERVER_ID = "srcFolderServerId"; /** Column name for the server-side id for dstFolderKey. */ public static final String DST_FOLDER_SERVER_ID = "dstFolderServerId"; /** Selection to get the last synced folder for a message. */ private static final String SELECTION_LAST_SYNCED_MAILBOX = MESSAGE_KEY + "=? and " + STATUS + "!=" + STATUS_FAILED_STRING; /** * Projection for a query to get all columns necessary for an actual move. */ private interface ProjectionMoveQuery { public static final int COLUMN_ID = 0; public static final int COLUMN_MESSAGE_KEY = 1; public static final int COLUMN_SERVER_ID = 2; public static final int COLUMN_SRC_FOLDER_KEY = 3; public static final int COLUMN_DST_FOLDER_KEY = 4; public static final int COLUMN_SRC_FOLDER_SERVER_ID = 5; public static final int COLUMN_DST_FOLDER_SERVER_ID = 6; public static final String[] PROJECTION = new String[] { ID, MESSAGE_KEY, SERVER_ID, SRC_FOLDER_KEY, DST_FOLDER_KEY, SRC_FOLDER_SERVER_ID, DST_FOLDER_SERVER_ID }; } /** * Projection for a query to get the original folder id for a message. */ private interface ProjectionLastSyncedMailboxQuery { public static final int COLUMN_ID = 0; public static final int COLUMN_SRC_FOLDER_KEY = 1; public static final String[] PROJECTION = new String[] { ID, SRC_FOLDER_KEY }; } // The actual fields. private final long mSrcFolderKey; private long mDstFolderKey; private final String mSrcFolderServerId; private String mDstFolderServerId; private MessageMove(final long messageKey,final String serverId, final long id, final long srcFolderKey, final long dstFolderKey, final String srcFolderServerId, final String dstFolderServerId) { super(messageKey, serverId, id); mSrcFolderKey = srcFolderKey; mDstFolderKey = dstFolderKey; mSrcFolderServerId = srcFolderServerId; mDstFolderServerId = dstFolderServerId; } public final long getSourceFolderKey() { return mSrcFolderKey; } public final String getSourceFolderId() { return mSrcFolderServerId; } public final String getDestFolderId() { return mDstFolderServerId; } /** * Initialize static state for this class. */ public static void init() { CONTENT_URI = EmailContent.CONTENT_URI.buildUpon().appendEncodedPath(PATH).build(); } /** * Get the final moves that we want to upsync to the server, setting the status in the DB for * all rows to {@link #STATUS_PROCESSING} that are being updated and to {@link #STATUS_FAILED} * for any old updates. * Messages whose sequence of pending moves results in a no-op (i.e. the message has been moved * back to its original folder) have their moves cleared from the DB without any upsync. * @param context A {@link Context}. * @param accountId The account we want to update. * @return The final moves to send to the server, or null if there are none. */ public static List getMoves(final Context context, final long accountId) { final ContentResolver cr = context.getContentResolver(); final Cursor c = getCursor(cr, CONTENT_URI, ProjectionMoveQuery.PROJECTION, accountId); if (c == null) { return null; } // Collapse any rows in the cursor that are acting on the same message. We know the cursor // returned by getRowsToProcess is ordered from oldest to newest, and we use this fact to // get the original and final folder for the message. LongSparseArray movesMap = new LongSparseArray(); try { while (c.moveToNext()) { final long id = c.getLong(ProjectionMoveQuery.COLUMN_ID); final long messageKey = c.getLong(ProjectionMoveQuery.COLUMN_MESSAGE_KEY); final String serverId = c.getString(ProjectionMoveQuery.COLUMN_SERVER_ID); final long srcFolderKey = c.getLong(ProjectionMoveQuery.COLUMN_SRC_FOLDER_KEY); final long dstFolderKey = c.getLong(ProjectionMoveQuery.COLUMN_DST_FOLDER_KEY); final String srcFolderServerId = c.getString(ProjectionMoveQuery.COLUMN_SRC_FOLDER_SERVER_ID); final String dstFolderServerId = c.getString(ProjectionMoveQuery.COLUMN_DST_FOLDER_SERVER_ID); final MessageMove existingMove = movesMap.get(messageKey); if (existingMove != null) { if (existingMove.mLastId >= id) { LogUtils.w(LOG_TAG, "Moves were not in ascending id order"); } if (!existingMove.mDstFolderServerId.equals(srcFolderServerId) || existingMove.mDstFolderKey != srcFolderKey) { LogUtils.w(LOG_TAG, "existing move's dst not same as this move's src"); } existingMove.mDstFolderKey = dstFolderKey; existingMove.mDstFolderServerId = dstFolderServerId; existingMove.mLastId = id; } else { movesMap.put(messageKey, new MessageMove(messageKey, serverId, id, srcFolderKey, dstFolderKey, srcFolderServerId, dstFolderServerId)); } } } finally { c.close(); } // Prune any no-op moves (i.e. messages that have been moved back to the initial folder). final int moveCount = movesMap.size(); final long[] unmovedMessages = new long[moveCount]; int unmovedMessagesCount = 0; final ArrayList moves = new ArrayList(moveCount); for (int i = 0; i < movesMap.size(); ++i) { final MessageMove move = movesMap.valueAt(i); // We also treat changes without a server id as a no-op. if ((move.mServerId == null || move.mServerId.length() == 0) || move.mSrcFolderKey == move.mDstFolderKey) { unmovedMessages[unmovedMessagesCount] = move.mMessageKey; ++unmovedMessagesCount; } else { moves.add(move); } } if (unmovedMessagesCount != 0) { deleteRowsForMessages(cr, CONTENT_URI, unmovedMessages, unmovedMessagesCount); } if (moves.isEmpty()) { return null; } return moves; } /** * Clean up the table to reflect a successful set of upsyncs. * @param cr A {@link ContentResolver} * @param messageKeys The messages to update. * @param count The number of messages. */ public static void upsyncSuccessful(final ContentResolver cr, final long[] messageKeys, final int count) { deleteRowsForMessages(cr, CONTENT_URI, messageKeys, count); } /** * Clean up the table to reflect upsyncs that need to be retried. * @param cr A {@link ContentResolver} * @param messageKeys The messages to update. * @param count The number of messages. */ public static void upsyncRetry(final ContentResolver cr, final long[] messageKeys, final int count) { retryMessages(cr, CONTENT_URI, messageKeys, count); } /** * Clean up the table to reflect upsyncs that failed and need to be reverted. * @param cr A {@link ContentResolver} * @param messageKeys The messages to update. * @param count The number of messages. */ public static void upsyncFail(final ContentResolver cr, final long[] messageKeys, final int count) { failMessages(cr, CONTENT_URI, messageKeys, count); } /** * Get the id for the mailbox this message is in (from the server's point of view). * @param cr A {@link ContentResolver}. * @param messageId The message we're interested in. * @return The id for the mailbox this message was in. */ public static long getLastSyncedMailboxForMessage(final ContentResolver cr, final long messageId) { // Check if there's a pending move and get the original mailbox id. final String[] selectionArgs = { String.valueOf(messageId) }; final Cursor moveCursor = cr.query(CONTENT_URI, ProjectionLastSyncedMailboxQuery.PROJECTION, SELECTION_LAST_SYNCED_MAILBOX, selectionArgs, ID + " ASC"); if (moveCursor != null) { try { if (moveCursor.moveToFirst()) { // We actually only care about the oldest one, i.e. the one we last got // from the server before we started mucking with it. return moveCursor.getLong( ProjectionLastSyncedMailboxQuery.COLUMN_SRC_FOLDER_KEY); } } finally { moveCursor.close(); } } // There are no pending moves for this message, so use the one in the Message table. final Cursor messageCursor = cr.query(ContentUris.withAppendedId( EmailContent.Message.CONTENT_URI, messageId), EmailContent.Message.MAILBOX_KEY_PROJECTION, null, null, null); if (messageCursor != null) { try { if (messageCursor.moveToFirst()) { return messageCursor.getLong(0); } } finally { messageCursor.close(); } } return Mailbox.NO_MAILBOX; } }