1package com.android.emailcommon.provider;
2
3import android.content.ContentResolver;
4import android.content.ContentValues;
5import android.database.Cursor;
6import 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 */
16public 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
53    protected MessageChangeLogTable(final long messageKey, final String serverId, final long id) {
54        mMessageKey = messageKey;
55        mServerId = serverId;
56        mLastId = id;
57    }
58
59    public final long getMessageId() {
60        return mMessageKey;
61    }
62
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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