| package com.android.emailcommon.provider; |
| |
| import android.content.ContentResolver; |
| import android.content.ContentValues; |
| import android.database.Cursor; |
| import android.net.Uri; |
| |
| /** |
| * {@link EmailContent}-like base class for change log tables. |
| * Accounts that upsync message changes require a change log to track local changes between upsyncs. |
| * A single instance of this class (or subclass) represents one change to upsync to the server. |
| * This object may actually correspond to multiple rows in the table. |
| * This class (and subclasses) also contains constants for the table columns and values stored in |
| * the DB. The base class contains the ones common to all change logs. |
| */ |
| public abstract class MessageChangeLogTable { |
| |
| // DB columns. Note that this class (and subclasses) use some denormalized columns |
| // (e.g. accountKey) for simplicity at query time and debugging ease. |
| /** Column name for the row key; this is an autoincrement key. */ |
| public static final String ID = "_id"; |
| /** Column name for a foreign key into Message for the message that's moving. */ |
| public static final String MESSAGE_KEY = "messageKey"; |
| /** Column name for the server-side id for messageKey. */ |
| public static final String SERVER_ID = "messageServerId"; |
| /** Column name for a foreign key into Account for the message that's moving. */ |
| public static final String ACCOUNT_KEY = "accountKey"; |
| /** Column name for a status value indicating where we are with processing this move request. */ |
| public static final String STATUS = "status"; |
| |
| // Status values. |
| /** Status value indicating this move has not yet been unpsynced. */ |
| public static final int STATUS_NONE = 0; |
| public static final String STATUS_NONE_STRING = String.valueOf(STATUS_NONE); |
| /** Status value indicating this move is being upsynced right now. */ |
| public static final int STATUS_PROCESSING = 1; |
| public static final String STATUS_PROCESSING_STRING = String.valueOf(STATUS_PROCESSING); |
| /** Status value indicating this move failed to upsync. */ |
| public static final int STATUS_FAILED = 2; |
| public static final String STATUS_FAILED_STRING = String.valueOf(STATUS_FAILED); |
| |
| /** Selection string for querying this table. */ |
| private static final String SELECTION_BY_ACCOUNT_KEY_AND_STATUS = |
| ACCOUNT_KEY + "=? and " + STATUS + "=?"; |
| |
| /** Selection string prefix for deleting moves for a set of messages. */ |
| private static final String SELECTION_BY_MESSAGE_KEYS_PREFIX = MESSAGE_KEY + " in ("; |
| |
| protected final long mMessageKey; |
| protected final String mServerId; |
| protected long mLastId; |
| |
| protected MessageChangeLogTable(final long messageKey, final String serverId, final long id) { |
| mMessageKey = messageKey; |
| mServerId = serverId; |
| mLastId = id; |
| } |
| |
| public final long getMessageId() { |
| return mMessageKey; |
| } |
| |
| public final String getServerId() { |
| return mServerId; |
| } |
| |
| /** |
| * Update status of all change entries for an account: |
| * - {@link #STATUS_NONE} -> {@link #STATUS_PROCESSING} |
| * - {@link #STATUS_PROCESSING} -> {@link #STATUS_FAILED} |
| * @param cr A {@link ContentResolver}. |
| * @param uri The content uri for this table. |
| * @param accountId The account we want to update. |
| * @return The number of change entries that are now in {@link #STATUS_PROCESSING}. |
| */ |
| private static int startProcessing(final ContentResolver cr, final Uri uri, |
| final String accountId) { |
| final String[] args = new String[2]; |
| args[0] = accountId; |
| final ContentValues cv = new ContentValues(1); |
| |
| // First mark anything that's still processing as failed. |
| args[1] = STATUS_PROCESSING_STRING; |
| cv.put(STATUS, STATUS_FAILED); |
| cr.update(uri, cv, SELECTION_BY_ACCOUNT_KEY_AND_STATUS, args); |
| |
| // Now mark all unprocessed messages as processing. |
| args[1] = STATUS_NONE_STRING; |
| cv.put(STATUS, STATUS_PROCESSING); |
| return cr.update(uri, cv, SELECTION_BY_ACCOUNT_KEY_AND_STATUS, args); |
| } |
| |
| /** |
| * Query for all move records that are in {@link #STATUS_PROCESSING}. |
| * Note that this function assumes the underlying table uses an autoincrement id key: it assumes |
| * that ascending id is the same as chronological order. |
| * @param cr A {@link ContentResolver}. |
| * @param uri The content uri for this table. |
| * @param projection The projection to use for this query. |
| * @param accountId The account we want to update. |
| * @return A {@link android.database.Cursor} containing all rows, in id order. |
| */ |
| private static Cursor getRowsToProcess(final ContentResolver cr, final Uri uri, |
| final String[] projection, final String accountId) { |
| final String[] args = { accountId, STATUS_PROCESSING_STRING }; |
| return cr.query(uri, projection, SELECTION_BY_ACCOUNT_KEY_AND_STATUS, args, ID + " ASC"); |
| } |
| |
| /** |
| * Create a selection string for all messages in a set. |
| * @param messageKeys The set of messages we're interested in. |
| * @param count The number of messages we're interested in. |
| * @return The selection string for these messages. |
| */ |
| private static String getSelectionForMessages(final long[] messageKeys, final int count) { |
| final StringBuilder sb = new StringBuilder(SELECTION_BY_MESSAGE_KEYS_PREFIX); |
| for (int i = 0; i < count; ++i) { |
| if (i != 0) { |
| sb.append(","); |
| } |
| sb.append(messageKeys[i]); |
| } |
| sb.append(")"); |
| return sb.toString(); |
| } |
| |
| /** |
| * Delete all rows for a set of messages. Used to clear no-op changes (i.e. multiple rows for |
| * a message that reverts it to the original state) and after successful upsync. |
| * @param cr A {@link ContentResolver}. |
| * @param uri The content uri for this table. |
| * @param messageKeys The messages to clear. |
| * @param count The number of message keys. |
| * @return The number of rows deleted from the DB. |
| */ |
| protected static int deleteRowsForMessages(final ContentResolver cr, final Uri uri, |
| final long[] messageKeys, final int count) { |
| if (count == 0) { |
| return 0; |
| } |
| return cr.delete(uri, getSelectionForMessages(messageKeys, count), null); |
| } |
| |
| /** |
| * Set the status value for a set of messages. |
| * @param cr A {@link ContentResolver}. |
| * @param uri The {@link Uri} for the update. |
| * @param messageKeys The messages to update. |
| * @param count The number of messageKeys. |
| * @param status The new status value for the messages. |
| * @return The number of rows updated. |
| */ |
| private static int updateStatusForMessages(final ContentResolver cr, final Uri uri, |
| final long[] messageKeys, final int count, final int status) { |
| if (count == 0) { |
| return 0; |
| } |
| final ContentValues cv = new ContentValues(1); |
| cv.put(STATUS, status); |
| return cr.update(uri, cv, getSelectionForMessages(messageKeys, count), null); |
| } |
| |
| /** |
| * Set a set of messages to status = retry. |
| * @param cr A {@link ContentResolver}. |
| * @param uri The {@link Uri} for the update. |
| * @param messageKeys The messages to update. |
| * @param count The number of messageKeys. |
| * @return The number of rows updated. |
| */ |
| protected static int retryMessages(final ContentResolver cr, final Uri uri, |
| final long[] messageKeys, final int count) { |
| return updateStatusForMessages(cr, uri, messageKeys, count, STATUS_NONE); |
| } |
| |
| /** |
| * Set a set of messages to status = failed. |
| * @param cr A {@link ContentResolver}. |
| * @param uri The {@link Uri} for the update. |
| * @param messageKeys The messages to update. |
| * @param count The number of messageKeys. |
| * @return The number of rows updated. |
| */ |
| protected static int failMessages(final ContentResolver cr, final Uri uri, |
| final long[] messageKeys, final int count) { |
| return updateStatusForMessages(cr, uri, messageKeys, count, STATUS_FAILED); |
| } |
| |
| /** |
| * Start processing our table and get a {@link Cursor} for the rows to process. |
| * @param cr A {@link ContentResolver}. |
| * @param uri The {@link Uri} for the update. |
| * @param projection The projection to use for our read. |
| * @param accountId The account we're interested in. |
| * @return A {@link Cursor} with the change log rows we're interested in. |
| */ |
| protected static Cursor getCursor(final ContentResolver cr, final Uri uri, |
| final String[] projection, final long accountId) { |
| final String accountIdString = String.valueOf(accountId); |
| if (startProcessing(cr, uri, accountIdString) <= 0) { |
| return null; |
| } |
| return getRowsToProcess(cr, uri, projection, accountIdString); |
| } |
| } |