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