| package com.android.emailcommon.provider; |
| |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.support.v4.util.LongSparseArray; |
| |
| import com.android.mail.utils.LogUtils; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * {@link EmailContent}-like class for the MessageStateChange table. |
| */ |
| public class MessageStateChange extends MessageChangeLogTable { |
| /** Logging tag. */ |
| public static final String LOG_TAG = "MessageStateChange"; |
| |
| /** The name for this table in the database. */ |
| public static final String TABLE_NAME = "MessageStateChange"; |
| |
| /** The path for the URI for interacting with message moves. */ |
| public static final String PATH = "messageChange"; |
| |
| /** The URI for dealing with message move data. */ |
| public static Uri CONTENT_URI; |
| |
| // DB columns. |
| /** Column name for the old value of flagRead. */ |
| public static final String OLD_FLAG_READ = "oldFlagRead"; |
| /** Column name for the new value of flagRead. */ |
| public static final String NEW_FLAG_READ = "newFlagRead"; |
| /** Column name for the old value of flagFavorite. */ |
| public static final String OLD_FLAG_FAVORITE = "oldFlagFavorite"; |
| /** Column name for the new value of flagFavorite. */ |
| public static final String NEW_FLAG_FAVORITE = "newFlagFavorite"; |
| |
| /** Value stored in DB for "new" columns when an update did not touch this particular value. */ |
| public static final int VALUE_UNCHANGED = -1; |
| |
| /** |
| * Projection for a query to get all columns necessary for an actual change. |
| */ |
| private interface ProjectionChangeQuery { |
| public static final int COLUMN_ID = 0; |
| public static final int COLUMN_MESSAGE_KEY = 1; |
| public static final int COLUMN_SERVER_ID = 2; |
| public static final int COLUMN_OLD_FLAG_READ = 3; |
| public static final int COLUMN_NEW_FLAG_READ = 4; |
| public static final int COLUMN_OLD_FLAG_FAVORITE = 5; |
| public static final int COLUMN_NEW_FLAG_FAVORITE = 6; |
| |
| public static final String[] PROJECTION = new String[] { |
| ID, MESSAGE_KEY, SERVER_ID, |
| OLD_FLAG_READ, NEW_FLAG_READ, |
| OLD_FLAG_FAVORITE, NEW_FLAG_FAVORITE |
| }; |
| } |
| |
| // The actual fields. |
| private final int mOldFlagRead; |
| private int mNewFlagRead; |
| private final int mOldFlagFavorite; |
| private int mNewFlagFavorite; |
| private final long mMailboxId; |
| |
| private 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) { |
| super(messageKey, serverId, id); |
| mOldFlagRead = oldFlagRead; |
| mNewFlagRead = newFlagRead; |
| mOldFlagFavorite = oldFlagFavorite; |
| mNewFlagFavorite = newFlagFavorite; |
| mMailboxId = mailboxId; |
| } |
| |
| public final int getNewFlagRead() { |
| if (mOldFlagRead == mNewFlagRead) { |
| return VALUE_UNCHANGED; |
| } |
| return mNewFlagRead; |
| } |
| |
| public final int getNewFlagFavorite() { |
| if (mOldFlagFavorite == mNewFlagFavorite) { |
| return VALUE_UNCHANGED; |
| } |
| return mNewFlagFavorite; |
| } |
| |
| /** |
| * Initialize static state for this class. |
| */ |
| public static void init() { |
| CONTENT_URI = EmailContent.CONTENT_URI.buildUpon().appendEncodedPath(PATH).build(); |
| } |
| |
| /** |
| * Gets final state changes to upsync to the server, setting the status in the DB for all rows |
| * to {@link #STATUS_PROCESSING} that are being updated and to {@link #STATUS_FAILED} for any |
| * old updates. Messages whose sequence of changes results in a no-op are cleared from the DB |
| * without any upsync. |
| * @param context A {@link Context}. |
| * @param accountId The account we want to update. |
| * @param ignoreFavorites Whether to ignore changes to the favorites flag. |
| * @return The final chnages to send to the server, or null if there are none. |
| */ |
| public static List<MessageStateChange> getChanges(final Context context, final long accountId, |
| final boolean ignoreFavorites) { |
| final ContentResolver cr = context.getContentResolver(); |
| final Cursor c = getCursor(cr, CONTENT_URI, ProjectionChangeQuery.PROJECTION, accountId); |
| if (c == null) { |
| return null; |
| } |
| |
| // Collapse rows acting on the same message. |
| // TODO: Unify with MessageMove, move to base class as much as possible. |
| LongSparseArray<MessageStateChange> changesMap = new LongSparseArray(); |
| try { |
| while (c.moveToNext()) { |
| final long id = c.getLong(ProjectionChangeQuery.COLUMN_ID); |
| final long messageKey = c.getLong(ProjectionChangeQuery.COLUMN_MESSAGE_KEY); |
| final String serverId = c.getString(ProjectionChangeQuery.COLUMN_SERVER_ID); |
| final int oldFlagRead = c.getInt(ProjectionChangeQuery.COLUMN_OLD_FLAG_READ); |
| final int newFlagReadTable = c.getInt(ProjectionChangeQuery.COLUMN_NEW_FLAG_READ); |
| final int newFlagRead = (newFlagReadTable == VALUE_UNCHANGED) ? |
| oldFlagRead : newFlagReadTable; |
| final int oldFlagFavorite = |
| c.getInt(ProjectionChangeQuery.COLUMN_OLD_FLAG_FAVORITE); |
| final int newFlagFavoriteTable = |
| c.getInt(ProjectionChangeQuery.COLUMN_NEW_FLAG_FAVORITE); |
| final int newFlagFavorite = |
| (ignoreFavorites || newFlagFavoriteTable == VALUE_UNCHANGED) ? |
| oldFlagFavorite : newFlagFavoriteTable; |
| final MessageStateChange existingChange = changesMap.get(messageKey); |
| if (existingChange != null) { |
| if (existingChange.mLastId >= id) { |
| LogUtils.w(LOG_TAG, "DChanges were not in ascending id order"); |
| } |
| if (existingChange.mNewFlagRead != oldFlagRead || |
| existingChange.mNewFlagFavorite != oldFlagFavorite) { |
| LogUtils.w(LOG_TAG, "existing change inconsistent with new change"); |
| } |
| existingChange.mNewFlagRead = newFlagRead; |
| existingChange.mNewFlagFavorite = newFlagFavorite; |
| existingChange.mLastId = id; |
| } else { |
| final long mailboxId = MessageMove.getLastSyncedMailboxForMessage(cr, |
| messageKey); |
| if (mailboxId == Mailbox.NO_MAILBOX) { |
| LogUtils.e(LOG_TAG, "No mailbox id for message %d", messageKey); |
| } else { |
| changesMap.put(messageKey, new MessageStateChange(messageKey, serverId, id, |
| oldFlagRead, newFlagRead, oldFlagFavorite, newFlagFavorite, |
| mailboxId)); |
| } |
| } |
| } |
| } finally { |
| c.close(); |
| } |
| |
| // Prune no-ops. |
| // TODO: Unify with MessageMove, move to base class as much as possible. |
| final int count = changesMap.size(); |
| final long[] unchangedMessages = new long[count]; |
| int unchangedMessagesCount = 0; |
| final ArrayList<MessageStateChange> changes = new ArrayList(count); |
| for (int i = 0; i < changesMap.size(); ++i) { |
| final MessageStateChange change = changesMap.valueAt(i); |
| // We also treat changes without a server id as a no-op. |
| if ((change.mServerId == null || change.mServerId.length() == 0) || |
| (change.mOldFlagRead == change.mNewFlagRead && |
| change.mOldFlagFavorite == change.mNewFlagFavorite)) { |
| unchangedMessages[unchangedMessagesCount] = change.mMessageKey; |
| ++unchangedMessagesCount; |
| } else { |
| changes.add(change); |
| } |
| } |
| if (unchangedMessagesCount != 0) { |
| deleteRowsForMessages(cr, CONTENT_URI, unchangedMessages, unchangedMessagesCount); |
| } |
| if (changes.isEmpty()) { |
| return null; |
| } |
| return changes; |
| } |
| |
| /** |
| * Rearrange the changes list to a map by mailbox id. |
| * @return The final changes to send to the server, or null if there are none. |
| */ |
| public static LongSparseArray<List<MessageStateChange>> convertToChangesMap( |
| final List<MessageStateChange> changes) { |
| if (changes == null) { |
| return null; |
| } |
| |
| final LongSparseArray<List<MessageStateChange>> changesMap = new LongSparseArray(); |
| for (final MessageStateChange change : changes) { |
| List<MessageStateChange> list = changesMap.get(change.mMailboxId); |
| if (list == null) { |
| list = new ArrayList(); |
| changesMap.put(change.mMailboxId, list); |
| } |
| list.add(change); |
| } |
| if (changesMap.size() == 0) { |
| return null; |
| } |
| return changesMap; |
| } |
| |
| /** |
| * Clean up the table to reflect a successful set of upsyncs. |
| * @param cr A {@link ContentResolver} |
| * @param messageKeys The messages to update. |
| * @param count The number of messages. |
| */ |
| public static void upsyncSuccessful(final ContentResolver cr, final long[] messageKeys, |
| final int count) { |
| deleteRowsForMessages(cr, CONTENT_URI, messageKeys, count); |
| } |
| |
| /** |
| * Clean up the table to reflect upsyncs that need to be retried. |
| * @param cr A {@link ContentResolver} |
| * @param messageKeys The messages to update. |
| * @param count The number of messages. |
| */ |
| public static void upsyncRetry(final ContentResolver cr, final long[] messageKeys, |
| final int count) { |
| retryMessages(cr, CONTENT_URI, messageKeys, count); |
| } |
| } |