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 androidx.collection.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 MessageMove(final long messageKey,final String serverId, final long id, final long srcFolderKey, final long dstFolderKey, final String srcFolderServerId, final String dstFolderServerId)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 getSourceFolderKey()90 public final long getSourceFolderKey() { 91 return mSrcFolderKey; 92 } 93 getSourceFolderId()94 public final String getSourceFolderId() { 95 return mSrcFolderServerId; 96 } 97 getDestFolderId()98 public final String getDestFolderId() { 99 return mDstFolderServerId; 100 } 101 102 /** 103 * Initialize static state for this class. 104 */ init()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 */ getMoves(final Context context, final long accountId)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 */ upsyncSuccessful(final ContentResolver cr, final long[] messageKeys, final int count)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 */ upsyncRetry(final ContentResolver cr, final long[] messageKeys, final int count)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 */ upsyncFail(final ContentResolver cr, final long[] messageKeys, final int count)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 */ getLastSyncedMailboxForMessage(final ContentResolver cr, final long messageId)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