1package com.android.emailcommon.provider;
2
3import android.content.ContentResolver;
4import android.content.Context;
5import android.database.Cursor;
6import android.net.Uri;
7import android.support.v4.util.LongSparseArray;
8
9import com.android.mail.utils.LogUtils;
10
11import java.util.ArrayList;
12import java.util.List;
13
14/**
15 * {@link EmailContent}-like class for the MessageStateChange table.
16 */
17public 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
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
81    public final int getNewFlagRead() {
82        if (mOldFlagRead == mNewFlagRead) {
83            return VALUE_UNCHANGED;
84        }
85        return mNewFlagRead;
86    }
87
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     */
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     */
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     */
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     */
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     */
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