1 package com.android.emailcommon.provider; 2 3 import android.content.ContentResolver; 4 import android.content.ContentValues; 5 import android.database.Cursor; 6 import android.net.Uri; 7 8 /** 9 * {@link EmailContent}-like base class for change log tables. 10 * Accounts that upsync message changes require a change log to track local changes between upsyncs. 11 * A single instance of this class (or subclass) represents one change to upsync to the server. 12 * This object may actually correspond to multiple rows in the table. 13 * This class (and subclasses) also contains constants for the table columns and values stored in 14 * the DB. The base class contains the ones common to all change logs. 15 */ 16 public abstract class MessageChangeLogTable { 17 18 // DB columns. Note that this class (and subclasses) use some denormalized columns 19 // (e.g. accountKey) for simplicity at query time and debugging ease. 20 /** Column name for the row key; this is an autoincrement key. */ 21 public static final String ID = "_id"; 22 /** Column name for a foreign key into Message for the message that's moving. */ 23 public static final String MESSAGE_KEY = "messageKey"; 24 /** Column name for the server-side id for messageKey. */ 25 public static final String SERVER_ID = "messageServerId"; 26 /** Column name for a foreign key into Account for the message that's moving. */ 27 public static final String ACCOUNT_KEY = "accountKey"; 28 /** Column name for a status value indicating where we are with processing this move request. */ 29 public static final String STATUS = "status"; 30 31 // Status values. 32 /** Status value indicating this move has not yet been unpsynced. */ 33 public static final int STATUS_NONE = 0; 34 public static final String STATUS_NONE_STRING = String.valueOf(STATUS_NONE); 35 /** Status value indicating this move is being upsynced right now. */ 36 public static final int STATUS_PROCESSING = 1; 37 public static final String STATUS_PROCESSING_STRING = String.valueOf(STATUS_PROCESSING); 38 /** Status value indicating this move failed to upsync. */ 39 public static final int STATUS_FAILED = 2; 40 public static final String STATUS_FAILED_STRING = String.valueOf(STATUS_FAILED); 41 42 /** Selection string for querying this table. */ 43 private static final String SELECTION_BY_ACCOUNT_KEY_AND_STATUS = 44 ACCOUNT_KEY + "=? and " + STATUS + "=?"; 45 46 /** Selection string prefix for deleting moves for a set of messages. */ 47 private static final String SELECTION_BY_MESSAGE_KEYS_PREFIX = MESSAGE_KEY + " in ("; 48 49 protected final long mMessageKey; 50 protected final String mServerId; 51 protected long mLastId; 52 MessageChangeLogTable(final long messageKey, final String serverId, final long id)53 protected MessageChangeLogTable(final long messageKey, final String serverId, final long id) { 54 mMessageKey = messageKey; 55 mServerId = serverId; 56 mLastId = id; 57 } 58 getMessageId()59 public final long getMessageId() { 60 return mMessageKey; 61 } 62 getServerId()63 public final String getServerId() { 64 return mServerId; 65 } 66 67 /** 68 * Update status of all change entries for an account: 69 * - {@link #STATUS_NONE} -> {@link #STATUS_PROCESSING} 70 * - {@link #STATUS_PROCESSING} -> {@link #STATUS_FAILED} 71 * @param cr A {@link ContentResolver}. 72 * @param uri The content uri for this table. 73 * @param accountId The account we want to update. 74 * @return The number of change entries that are now in {@link #STATUS_PROCESSING}. 75 */ startProcessing(final ContentResolver cr, final Uri uri, final String accountId)76 private static int startProcessing(final ContentResolver cr, final Uri uri, 77 final String accountId) { 78 final String[] args = new String[2]; 79 args[0] = accountId; 80 final ContentValues cv = new ContentValues(1); 81 82 // First mark anything that's still processing as failed. 83 args[1] = STATUS_PROCESSING_STRING; 84 cv.put(STATUS, STATUS_FAILED); 85 cr.update(uri, cv, SELECTION_BY_ACCOUNT_KEY_AND_STATUS, args); 86 87 // Now mark all unprocessed messages as processing. 88 args[1] = STATUS_NONE_STRING; 89 cv.put(STATUS, STATUS_PROCESSING); 90 return cr.update(uri, cv, SELECTION_BY_ACCOUNT_KEY_AND_STATUS, args); 91 } 92 93 /** 94 * Query for all move records that are in {@link #STATUS_PROCESSING}. 95 * Note that this function assumes the underlying table uses an autoincrement id key: it assumes 96 * that ascending id is the same as chronological order. 97 * @param cr A {@link ContentResolver}. 98 * @param uri The content uri for this table. 99 * @param projection The projection to use for this query. 100 * @param accountId The account we want to update. 101 * @return A {@link android.database.Cursor} containing all rows, in id order. 102 */ getRowsToProcess(final ContentResolver cr, final Uri uri, final String[] projection, final String accountId)103 private static Cursor getRowsToProcess(final ContentResolver cr, final Uri uri, 104 final String[] projection, final String accountId) { 105 final String[] args = { accountId, STATUS_PROCESSING_STRING }; 106 return cr.query(uri, projection, SELECTION_BY_ACCOUNT_KEY_AND_STATUS, args, ID + " ASC"); 107 } 108 109 /** 110 * Create a selection string for all messages in a set. 111 * @param messageKeys The set of messages we're interested in. 112 * @param count The number of messages we're interested in. 113 * @return The selection string for these messages. 114 */ getSelectionForMessages(final long[] messageKeys, final int count)115 private static String getSelectionForMessages(final long[] messageKeys, final int count) { 116 final StringBuilder sb = new StringBuilder(SELECTION_BY_MESSAGE_KEYS_PREFIX); 117 for (int i = 0; i < count; ++i) { 118 if (i != 0) { 119 sb.append(","); 120 } 121 sb.append(messageKeys[i]); 122 } 123 sb.append(")"); 124 return sb.toString(); 125 } 126 127 /** 128 * Delete all rows for a set of messages. Used to clear no-op changes (i.e. multiple rows for 129 * a message that reverts it to the original state) and after successful upsync. 130 * @param cr A {@link ContentResolver}. 131 * @param uri The content uri for this table. 132 * @param messageKeys The messages to clear. 133 * @param count The number of message keys. 134 * @return The number of rows deleted from the DB. 135 */ deleteRowsForMessages(final ContentResolver cr, final Uri uri, final long[] messageKeys, final int count)136 protected static int deleteRowsForMessages(final ContentResolver cr, final Uri uri, 137 final long[] messageKeys, final int count) { 138 if (count == 0) { 139 return 0; 140 } 141 return cr.delete(uri, getSelectionForMessages(messageKeys, count), null); 142 } 143 144 /** 145 * Set the status value for a set of messages. 146 * @param cr A {@link ContentResolver}. 147 * @param uri The {@link Uri} for the update. 148 * @param messageKeys The messages to update. 149 * @param count The number of messageKeys. 150 * @param status The new status value for the messages. 151 * @return The number of rows updated. 152 */ updateStatusForMessages(final ContentResolver cr, final Uri uri, final long[] messageKeys, final int count, final int status)153 private static int updateStatusForMessages(final ContentResolver cr, final Uri uri, 154 final long[] messageKeys, final int count, final int status) { 155 if (count == 0) { 156 return 0; 157 } 158 final ContentValues cv = new ContentValues(1); 159 cv.put(STATUS, status); 160 return cr.update(uri, cv, getSelectionForMessages(messageKeys, count), null); 161 } 162 163 /** 164 * Set a set of messages to status = retry. 165 * @param cr A {@link ContentResolver}. 166 * @param uri The {@link Uri} for the update. 167 * @param messageKeys The messages to update. 168 * @param count The number of messageKeys. 169 * @return The number of rows updated. 170 */ retryMessages(final ContentResolver cr, final Uri uri, final long[] messageKeys, final int count)171 protected static int retryMessages(final ContentResolver cr, final Uri uri, 172 final long[] messageKeys, final int count) { 173 return updateStatusForMessages(cr, uri, messageKeys, count, STATUS_NONE); 174 } 175 176 /** 177 * Set a set of messages to status = failed. 178 * @param cr A {@link ContentResolver}. 179 * @param uri The {@link Uri} for the update. 180 * @param messageKeys The messages to update. 181 * @param count The number of messageKeys. 182 * @return The number of rows updated. 183 */ failMessages(final ContentResolver cr, final Uri uri, final long[] messageKeys, final int count)184 protected static int failMessages(final ContentResolver cr, final Uri uri, 185 final long[] messageKeys, final int count) { 186 return updateStatusForMessages(cr, uri, messageKeys, count, STATUS_FAILED); 187 } 188 189 /** 190 * Start processing our table and get a {@link Cursor} for the rows to process. 191 * @param cr A {@link ContentResolver}. 192 * @param uri The {@link Uri} for the update. 193 * @param projection The projection to use for our read. 194 * @param accountId The account we're interested in. 195 * @return A {@link Cursor} with the change log rows we're interested in. 196 */ getCursor(final ContentResolver cr, final Uri uri, final String[] projection, final long accountId)197 protected static Cursor getCursor(final ContentResolver cr, final Uri uri, 198 final String[] projection, final long accountId) { 199 final String accountIdString = String.valueOf(accountId); 200 if (startProcessing(cr, uri, accountIdString) <= 0) { 201 return null; 202 } 203 return getRowsToProcess(cr, uri, projection, accountIdString); 204 } 205 } 206