1 package com.android.emailcommon.provider; 2 3 import android.content.ContentResolver; 4 import android.content.ContentUris; 5 import android.content.Context; 6 import android.database.Cursor; 7 import android.net.Uri; 8 import android.support.v4.util.LongSparseArray; 9 10 import com.android.mail.utils.LogUtils; 11 12 import java.util.ArrayList; 13 import java.util.List; 14 15 /** 16 * {@link EmailContent}-like class for the MessageMove table. 17 */ 18 public 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