Fix MessageStateChange to include mailbox id.

Also ignore messages without server ids for moves and
state changes.

Also cleanup to match needs of EAS upsync.

Bug: 10678136
Change-Id: Id4d5229b8479e61bd718b707b0d2bc77a9e68046
This commit is contained in:
Yu Ping Hu 2013-09-12 15:23:54 -07:00
parent ed610f749c
commit e984940465
6 changed files with 215 additions and 28 deletions

View File

@ -687,6 +687,8 @@ public abstract class EmailContent {
public static final String ACCOUNT_KEY_SELECTION =
MessageColumns.ACCOUNT_KEY + "=?";
public static final String[] MAILBOX_KEY_PROJECTION = new String[] { MAILBOX_KEY };
/**
* Selection for messages that are loaded
*

View File

@ -141,6 +141,18 @@ public class Mailbox extends EmailContent implements MailboxColumns, Parcelable
private static final String[] ACCOUNT_KEY_PROJECTION = { MailboxColumns.ACCOUNT_KEY };
private static final int ACCOUNT_KEY_PROJECTION_ACCOUNT_KEY_COLUMN = 0;
/**
* Projection for querying data needed during a sync.
*/
public interface ProjectionSyncData {
public static final int COLUMN_SERVER_ID = 0;
public static final int COLUMN_SYNC_KEY = 1;
public static final String[] PROJECTION = {
MailboxColumns.SERVER_ID, MailboxColumns.SYNC_KEY
};
};
public static final long NO_MAILBOX = -1;
// Sentinel values for the mSyncInterval field of both Mailbox records

View File

@ -37,6 +37,7 @@ public abstract class MessageChangeLogTable {
public static final String STATUS_PROCESSING_STRING = String.valueOf(STATUS_PROCESSING);
/** Status value indicating this move failed to upsync. */
public static final int STATUS_FAILED = 2;
public static final String STATUS_FAILED_STRING = String.valueOf(STATUS_FAILED);
/** Selection string for querying this table. */
private static final String SELECTION_BY_ACCOUNT_KEY_AND_STATUS =

View File

@ -1,6 +1,7 @@
package com.android.emailcommon.provider;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
@ -37,10 +38,14 @@ public class MessageMove extends MessageChangeLogTable {
/** Column name for the server-side id for dstFolderKey. */
public static final String DST_FOLDER_SERVER_ID = "dstFolderServerId";
/** Selection to get the last synced folder for a message. */
private static final String SELECTION_LAST_SYNCED_MAILBOX = MESSAGE_KEY + "=? and " + STATUS
+ "!=" + STATUS_FAILED_STRING;
/**
* Projection for a query to get all columns necessary for an actual move.
*/
private static final class ProjectionMoveQuery {
private interface ProjectionMoveQuery {
public static final int COLUMN_ID = 0;
public static final int COLUMN_MESSAGE_KEY = 1;
public static final int COLUMN_SERVER_ID = 2;
@ -56,6 +61,16 @@ public class MessageMove extends MessageChangeLogTable {
};
}
/**
* Projection for a query to get the original folder id for a message.
*/
private interface ProjectionLastSyncedMailboxQuery {
public static final int COLUMN_ID = 0;
public static final int COLUMN_SRC_FOLDER_KEY = 1;
public static final String[] PROJECTION = new String[] { ID, SRC_FOLDER_KEY };
}
// The actual fields.
private final long mSrcFolderKey;
private long mDstFolderKey;
@ -151,7 +166,9 @@ public class MessageMove extends MessageChangeLogTable {
final ArrayList<MessageMove> moves = new ArrayList(moveCount);
for (int i = 0; i < movesMap.size(); ++i) {
final MessageMove move = movesMap.valueAt(i);
if (move.mSrcFolderKey == move.mDstFolderKey) {
// We also treat changes without a server id as a no-op.
if ((move.mServerId == null || move.mServerId.length() == 0) ||
move.mSrcFolderKey == move.mDstFolderKey) {
unmovedMessages[unmovedMessagesCount] = move.mMessageKey;
++unmovedMessagesCount;
} else {
@ -199,4 +216,45 @@ public class MessageMove extends MessageChangeLogTable {
final int count) {
failMessages(cr, CONTENT_URI, messageKeys, count);
}
/**
* Get the id for the mailbox this message is in (from the server's point of view).
* @param cr A {@link ContentResolver}.
* @param messageId The message we're interested in.
* @return The id for the mailbox this message was in.
*/
public static long getLastSyncedMailboxForMessage(final ContentResolver cr,
final long messageId) {
// Check if there's a pending move and get the original mailbox id.
final String[] selectionArgs = { String.valueOf(messageId) };
final Cursor moveCursor = cr.query(CONTENT_URI, ProjectionLastSyncedMailboxQuery.PROJECTION,
SELECTION_LAST_SYNCED_MAILBOX, selectionArgs, ID + " ASC");
if (moveCursor != null) {
try {
if (moveCursor.moveToFirst()) {
// We actually only care about the oldest one, i.e. the one we last got
// from the server before we started mucking with it.
return moveCursor.getLong(
ProjectionLastSyncedMailboxQuery.COLUMN_SRC_FOLDER_KEY);
}
} finally {
moveCursor.close();
}
}
// There are no pending moves for this message, so use the one in the Message table.
final Cursor messageCursor = cr.query(ContentUris.withAppendedId(
EmailContent.Message.CONTENT_URI, messageId),
EmailContent.Message.MAILBOX_KEY_PROJECTION, null, null, null);
if (messageCursor != null) {
try {
if (messageCursor.moveToFirst()) {
return messageCursor.getLong(0);
}
} finally {
messageCursor.close();
}
}
return Mailbox.NO_MAILBOX;
}
}

View File

@ -43,7 +43,7 @@ public class MessageStateChange extends MessageChangeLogTable {
/**
* Projection for a query to get all columns necessary for an actual change.
*/
private static final class ProjectionChangeQuery {
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;
@ -64,15 +64,32 @@ public class MessageStateChange extends MessageChangeLogTable {
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 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;
}
/**
@ -82,7 +99,18 @@ public class MessageStateChange extends MessageChangeLogTable {
CONTENT_URI = EmailContent.CONTENT_URI.buildUpon().appendEncodedPath(PATH).build();
}
public static List<MessageStateChange> getChanges(final Context context, final long accountId) {
/**
* 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) {
@ -105,8 +133,9 @@ public class MessageStateChange extends MessageChangeLogTable {
c.getInt(ProjectionChangeQuery.COLUMN_OLD_FLAG_FAVORITE);
final int newFlagFavoriteTable =
c.getInt(ProjectionChangeQuery.COLUMN_NEW_FLAG_FAVORITE);
final int newFlagFavorite = (newFlagFavoriteTable == VALUE_UNCHANGED) ?
oldFlagFavorite : newFlagFavoriteTable;
final int newFlagFavorite =
(ignoreFavorites || newFlagFavoriteTable == VALUE_UNCHANGED) ?
oldFlagFavorite : newFlagFavoriteTable;
final MessageStateChange existingChange = changesMap.get(messageKey);
if (existingChange != null) {
if (existingChange.mLastId >= id) {
@ -120,8 +149,15 @@ public class MessageStateChange extends MessageChangeLogTable {
existingChange.mNewFlagFavorite = newFlagFavorite;
existingChange.mLastId = id;
} else {
changesMap.put(messageKey, new MessageStateChange(messageKey, serverId, id,
oldFlagRead, newFlagRead, oldFlagFavorite, newFlagFavorite));
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 {
@ -136,8 +172,10 @@ public class MessageStateChange extends MessageChangeLogTable {
final ArrayList<MessageStateChange> changes = new ArrayList(count);
for (int i = 0; i < changesMap.size(); ++i) {
final MessageStateChange change = changesMap.valueAt(i);
if (change.mOldFlagRead == change.mNewFlagRead &&
change.mOldFlagFavorite == change.mNewFlagFavorite) {
// 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 {
@ -152,4 +190,51 @@ public class MessageStateChange extends MessageChangeLogTable {
}
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);
}
}

View File

@ -1464,6 +1464,16 @@ public class EmailProvider extends ContentProvider {
"select count(*) from (select count(*) as dupes from " + Mailbox.TABLE_NAME +
" where accountKey=? group by " + MailboxColumns.SERVER_ID + ") where dupes > 1";
// Query to get the protocol for a message. Temporary to switch between new and old upsync
// behavior; should go away when IMAP gets converted.
private static final String GET_PROTOCOL_FOR_MESSAGE = "select h."
+ EmailContent.HostAuthColumns.PROTOCOL + " from "
+ Message.TABLE_NAME + " m inner join " + Account.TABLE_NAME + " a on m."
+ MessageColumns.ACCOUNT_KEY + "=a." + AccountColumns.ID + " inner join "
+ HostAuth.TABLE_NAME + " h on a." + AccountColumns.HOST_AUTH_KEY_RECV + "=h."
+ EmailContent.HostAuthColumns.ID + " where m." + MessageColumns.ID + "=?";
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
// Handle this special case the fastest possible way
@ -1555,24 +1565,43 @@ public class EmailProvider extends ContentProvider {
case POLICY_ID:
id = uri.getPathSegments().get(1);
if (match == SYNCED_MESSAGE_ID) {
// For synced messages, first copy the old message to the updated table
// Note the insert or ignore semantics, guaranteeing that only the first
// update will be reflected in the updated message table; therefore this
// row will always have the "original" data
db.execSQL(UPDATED_MESSAGE_INSERT + id);
Long dstFolderId = values.getAsLong(MessageColumns.MAILBOX_KEY);
if (dstFolderId != null) {
addToMessageMove(db, id, dstFolderId);
// TODO: Migrate IMAP to use MessageMove/MessageStateChange as well.
boolean isEas = false;
final Cursor c = db.rawQuery(GET_PROTOCOL_FOR_MESSAGE, new String[] {id});
if (c != null) {
try {
if (c.moveToFirst()) {
final String protocol = c.getString(0);
isEas = context.getString(R.string.protocol_eas)
.equals(protocol);
}
} finally {
c.close();
}
}
Integer flagRead = values.getAsInteger(MessageColumns.FLAG_READ);
Integer flagFavorite = values.getAsInteger(MessageColumns.FLAG_FAVORITE);
int flagReadValue = (flagRead != null) ?
flagRead : MessageStateChange.VALUE_UNCHANGED;
int flagFavoriteValue = (flagFavorite != null) ?
flagFavorite : MessageStateChange.VALUE_UNCHANGED;
if (flagRead != null || flagFavorite != null) {
addToMessageStateChange(db, id, flagReadValue, flagFavoriteValue);
if (isEas) {
// EAS uses the new upsync classes.
Long dstFolderId = values.getAsLong(MessageColumns.MAILBOX_KEY);
if (dstFolderId != null) {
addToMessageMove(db, id, dstFolderId);
}
Integer flagRead = values.getAsInteger(MessageColumns.FLAG_READ);
Integer flagFavorite = values.getAsInteger(MessageColumns.FLAG_FAVORITE);
int flagReadValue = (flagRead != null) ?
flagRead : MessageStateChange.VALUE_UNCHANGED;
int flagFavoriteValue = (flagFavorite != null) ?
flagFavorite : MessageStateChange.VALUE_UNCHANGED;
if (flagRead != null || flagFavorite != null) {
addToMessageStateChange(db, id, flagReadValue, flagFavoriteValue);
}
} else {
// Old way of doing upsync.
// For synced messages, first copy the old message to the updated table
// Note the insert or ignore semantics, guaranteeing that only the first
// update will be reflected in the updated message table; therefore this
// row will always have the "original" data
db.execSQL(UPDATED_MESSAGE_INSERT + id);
}
} else if (match == MESSAGE_ID) {
db.execSQL(UPDATED_MESSAGE_DELETE + id);