1 package com.android.emailcommon.provider;
2 
3 import android.content.ContentResolver;
4 import android.content.Context;
5 import android.database.Cursor;
6 import android.net.Uri;
7 import android.support.v4.util.LongSparseArray;
8 
9 import com.android.mail.utils.LogUtils;
10 
11 import java.util.ArrayList;
12 import java.util.List;
13 
14 /**
15  * {@link EmailContent}-like class for the MessageStateChange table.
16  */
17 public class MessageStateChange extends MessageChangeLogTable {
18     /** Logging tag. */
19     public static final String LOG_TAG = "MessageStateChange";
20 
21     /** The name for this table in the database. */
22     public static final String TABLE_NAME = "MessageStateChange";
23 
24     /** The path for the URI for interacting with message moves. */
25     public static final String PATH = "messageChange";
26 
27     /** The URI for dealing with message move data. */
28     public static Uri CONTENT_URI;
29 
30     // DB columns.
31     /** Column name for the old value of flagRead. */
32     public static final String OLD_FLAG_READ = "oldFlagRead";
33     /** Column name for the new value of flagRead. */
34     public static final String NEW_FLAG_READ = "newFlagRead";
35     /** Column name for the old value of flagFavorite. */
36     public static final String OLD_FLAG_FAVORITE = "oldFlagFavorite";
37     /** Column name for the new value of flagFavorite. */
38     public static final String NEW_FLAG_FAVORITE = "newFlagFavorite";
39 
40     /** Value stored in DB for "new" columns when an update did not touch this particular value. */
41     public static final int VALUE_UNCHANGED = -1;
42 
43     /**
44      * Projection for a query to get all columns necessary for an actual change.
45      */
46     private interface ProjectionChangeQuery {
47         public static final int COLUMN_ID = 0;
48         public static final int COLUMN_MESSAGE_KEY = 1;
49         public static final int COLUMN_SERVER_ID = 2;
50         public static final int COLUMN_OLD_FLAG_READ = 3;
51         public static final int COLUMN_NEW_FLAG_READ = 4;
52         public static final int COLUMN_OLD_FLAG_FAVORITE = 5;
53         public static final int COLUMN_NEW_FLAG_FAVORITE = 6;
54 
55         public static final String[] PROJECTION = new String[] {
56                 ID, MESSAGE_KEY, SERVER_ID,
57                 OLD_FLAG_READ, NEW_FLAG_READ,
58                 OLD_FLAG_FAVORITE, NEW_FLAG_FAVORITE
59         };
60     }
61 
62     // The actual fields.
63     private final int mOldFlagRead;
64     private int mNewFlagRead;
65     private final int mOldFlagFavorite;
66     private int mNewFlagFavorite;
67     private final long mMailboxId;
68 
MessageStateChange(final long messageKey,final String serverId, final long id, final int oldFlagRead, final int newFlagRead, final int oldFlagFavorite, final int newFlagFavorite, final long mailboxId)69     private MessageStateChange(final long messageKey,final String serverId, final long id,
70             final int oldFlagRead, final int newFlagRead,
71             final int oldFlagFavorite, final int newFlagFavorite,
72             final long mailboxId) {
73         super(messageKey, serverId, id);
74         mOldFlagRead = oldFlagRead;
75         mNewFlagRead = newFlagRead;
76         mOldFlagFavorite = oldFlagFavorite;
77         mNewFlagFavorite = newFlagFavorite;
78         mMailboxId = mailboxId;
79     }
80 
getNewFlagRead()81     public final int getNewFlagRead() {
82         if (mOldFlagRead == mNewFlagRead) {
83             return VALUE_UNCHANGED;
84         }
85         return mNewFlagRead;
86     }
87 
getNewFlagFavorite()88     public final int getNewFlagFavorite() {
89         if (mOldFlagFavorite == mNewFlagFavorite) {
90             return VALUE_UNCHANGED;
91         }
92         return mNewFlagFavorite;
93     }
94 
95     /**
96      * Initialize static state for this class.
97      */
init()98     public static void init() {
99         CONTENT_URI = EmailContent.CONTENT_URI.buildUpon().appendEncodedPath(PATH).build();
100     }
101 
102     /**
103      * Gets final state changes to upsync to the server, setting the status in the DB for all rows
104      * to {@link #STATUS_PROCESSING} that are being updated and to {@link #STATUS_FAILED} for any
105      * old updates. Messages whose sequence of changes results in a no-op are cleared from the DB
106      * without any upsync.
107      * @param context A {@link Context}.
108      * @param accountId The account we want to update.
109      * @param ignoreFavorites Whether to ignore changes to the favorites flag.
110      * @return The final chnages to send to the server, or null if there are none.
111      */
getChanges(final Context context, final long accountId, final boolean ignoreFavorites)112     public static List<MessageStateChange> getChanges(final Context context, final long accountId,
113             final boolean ignoreFavorites) {
114         final ContentResolver cr = context.getContentResolver();
115         final Cursor c = getCursor(cr, CONTENT_URI, ProjectionChangeQuery.PROJECTION, accountId);
116         if (c == null) {
117             return null;
118         }
119 
120         // Collapse rows acting on the same message.
121         // TODO: Unify with MessageMove, move to base class as much as possible.
122         LongSparseArray<MessageStateChange> changesMap = new LongSparseArray();
123         try {
124             while (c.moveToNext()) {
125                 final long id = c.getLong(ProjectionChangeQuery.COLUMN_ID);
126                 final long messageKey = c.getLong(ProjectionChangeQuery.COLUMN_MESSAGE_KEY);
127                 final String serverId = c.getString(ProjectionChangeQuery.COLUMN_SERVER_ID);
128                 final int oldFlagRead = c.getInt(ProjectionChangeQuery.COLUMN_OLD_FLAG_READ);
129                 final int newFlagReadTable =  c.getInt(ProjectionChangeQuery.COLUMN_NEW_FLAG_READ);
130                 final int newFlagRead = (newFlagReadTable == VALUE_UNCHANGED) ?
131                         oldFlagRead : newFlagReadTable;
132                 final int oldFlagFavorite =
133                         c.getInt(ProjectionChangeQuery.COLUMN_OLD_FLAG_FAVORITE);
134                 final int newFlagFavoriteTable =
135                         c.getInt(ProjectionChangeQuery.COLUMN_NEW_FLAG_FAVORITE);
136                 final int newFlagFavorite =
137                         (ignoreFavorites || newFlagFavoriteTable == VALUE_UNCHANGED) ?
138                                 oldFlagFavorite : newFlagFavoriteTable;
139                 final MessageStateChange existingChange = changesMap.get(messageKey);
140                 if (existingChange != null) {
141                     if (existingChange.mLastId >= id) {
142                         LogUtils.w(LOG_TAG, "DChanges were not in ascending id order");
143                     }
144                     if (existingChange.mNewFlagRead != oldFlagRead ||
145                             existingChange.mNewFlagFavorite != oldFlagFavorite) {
146                         LogUtils.w(LOG_TAG, "existing change inconsistent with new change");
147                     }
148                     existingChange.mNewFlagRead = newFlagRead;
149                     existingChange.mNewFlagFavorite = newFlagFavorite;
150                     existingChange.mLastId = id;
151                 } else {
152                     final long mailboxId = MessageMove.getLastSyncedMailboxForMessage(cr,
153                             messageKey);
154                     if (mailboxId == Mailbox.NO_MAILBOX) {
155                         LogUtils.e(LOG_TAG, "No mailbox id for message %d", messageKey);
156                     } else {
157                         changesMap.put(messageKey, new MessageStateChange(messageKey, serverId, id,
158                                 oldFlagRead, newFlagRead, oldFlagFavorite, newFlagFavorite,
159                                 mailboxId));
160                     }
161                 }
162             }
163         } finally {
164             c.close();
165         }
166 
167         // Prune no-ops.
168         // TODO: Unify with MessageMove, move to base class as much as possible.
169         final int count = changesMap.size();
170         final long[] unchangedMessages = new long[count];
171         int unchangedMessagesCount = 0;
172         final ArrayList<MessageStateChange> changes = new ArrayList(count);
173         for (int i = 0; i < changesMap.size(); ++i) {
174             final MessageStateChange change = changesMap.valueAt(i);
175             // We also treat changes without a server id as a no-op.
176             if ((change.mServerId == null || change.mServerId.length() == 0) ||
177                     (change.mOldFlagRead == change.mNewFlagRead &&
178                             change.mOldFlagFavorite == change.mNewFlagFavorite)) {
179                 unchangedMessages[unchangedMessagesCount] = change.mMessageKey;
180                 ++unchangedMessagesCount;
181             } else {
182                 changes.add(change);
183             }
184         }
185         if (unchangedMessagesCount != 0) {
186             deleteRowsForMessages(cr, CONTENT_URI, unchangedMessages, unchangedMessagesCount);
187         }
188         if (changes.isEmpty()) {
189             return null;
190         }
191         return changes;
192     }
193 
194     /**
195      * Rearrange the changes list to a map by mailbox id.
196      * @return The final changes to send to the server, or null if there are none.
197      */
convertToChangesMap( final List<MessageStateChange> changes)198     public static LongSparseArray<List<MessageStateChange>> convertToChangesMap(
199             final List<MessageStateChange> changes) {
200         if (changes == null) {
201             return null;
202         }
203 
204         final LongSparseArray<List<MessageStateChange>> changesMap = new LongSparseArray();
205         for (final MessageStateChange change : changes) {
206             List<MessageStateChange> list = changesMap.get(change.mMailboxId);
207             if (list == null) {
208                 list = new ArrayList();
209                 changesMap.put(change.mMailboxId, list);
210             }
211             list.add(change);
212         }
213         if (changesMap.size() == 0) {
214             return null;
215         }
216         return changesMap;
217     }
218 
219     /**
220      * Clean up the table to reflect a successful set of upsyncs.
221      * @param cr A {@link ContentResolver}
222      * @param messageKeys The messages to update.
223      * @param count The number of messages.
224      */
upsyncSuccessful(final ContentResolver cr, final long[] messageKeys, final int count)225     public static void upsyncSuccessful(final ContentResolver cr, final long[] messageKeys,
226             final int count) {
227         deleteRowsForMessages(cr, CONTENT_URI, messageKeys, count);
228     }
229 
230     /**
231      * Clean up the table to reflect upsyncs that need to be retried.
232      * @param cr A {@link ContentResolver}
233      * @param messageKeys The messages to update.
234      * @param count The number of messages.
235      */
upsyncRetry(final ContentResolver cr, final long[] messageKeys, final int count)236     public static void upsyncRetry(final ContentResolver cr, final long[] messageKeys,
237             final int count) {
238         retryMessages(cr, CONTENT_URI, messageKeys, count);
239     }
240 }
241