Add the MessageMove & MessageStateChange tables.

We need to track changes that need to be unsynced. Because
Exchange handles moves differently from other changes, we
create two different tables. The tables are structured as
change logs to better handle error cases.

Change-Id: I4df90c75f36707fa117aed9718508426e60e0749
This commit is contained in:
Yu Ping Hu 2013-09-05 13:52:03 -07:00
parent 582bc439ea
commit ca79aba675
7 changed files with 750 additions and 86 deletions

View File

@ -31,6 +31,7 @@ imported_unified_email_files := \
LOCAL_MODULE := com.android.emailcommon
LOCAL_STATIC_JAVA_LIBRARIES := guava android-common
LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4
LOCAL_SRC_FILES := $(call all-java-files-under, src/com/android/emailcommon)
LOCAL_SRC_FILES += \
src/com/android/emailcommon/service/IEmailService.aidl \

View File

@ -129,33 +129,6 @@ public abstract class EmailContent {
public static Uri MAILBOX_MOST_RECENT_MESSAGE_URI;
public static Uri ACCOUNT_CHECK_URI;
/** The path for the query to get the moved message info for an account. */
public static final String MOVED_MESSAGES_PATH = "movedMessages";
/**
* The URI to query for information regarding messages that have been moved on the client
* where this move has not yet been synced to the server.
* This query will return the columns in {@link MovedMessagesColumns}.
*/
public static Uri MOVED_MESSAGES_URI;
/**
* Columns returned by the moved messages query.
*/
public static final class MovedMessagesColumns {
/** The server side message id for the message that is moving. */
public static final String MESSAGE_ID = "messageId";
/** The local message id for the message that is moving. */
public static final String LOCAL_MESSAGE_ID = "localMessageId";
/** The server side folder id for the folder the message was in. */
public static final String SOURCE_FOLDER_ID = "sourceFolderId";
/** The local folder id for the folder the message was in. */
public static final String LOCAL_SOURCE_FOLDER_ID = "localSourceFolderId";
/** The server side folder id for the folder the message is moving to. */
public static final String DEST_FOLDER_ID = "destFolderId";
/** The local folder id for the folder the message is moving to. */
public static final String LOCAL_DEST_FOLDER_ID = "localDestFolderId";
}
public static String PROVIDER_PERMISSION;
public static synchronized void init(Context context) {
@ -173,8 +146,6 @@ public abstract class EmailContent {
MAILBOX_MOST_RECENT_MESSAGE_URI = Uri.parse("content://" + AUTHORITY +
"/mailboxMostRecentMessage");
ACCOUNT_CHECK_URI = Uri.parse("content://" + AUTHORITY + "/accountCheck");
MOVED_MESSAGES_URI =
CONTENT_URI.buildUpon().appendEncodedPath("/" + MOVED_MESSAGES_PATH).build();
PROVIDER_PERMISSION = EMAIL_PACKAGE_NAME + ".permission.ACCESS_PROVIDER";
// Initialize subclasses
Account.initAccount();
@ -183,6 +154,8 @@ public abstract class EmailContent {
HostAuth.initHostAuth();
Policy.initPolicy();
Message.initMessage();
MessageMove.init();
MessageStateChange.init();
Body.initBody();
Attachment.initAttachment();
}

View File

@ -0,0 +1,204 @@
package com.android.emailcommon.provider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
/**
* {@link EmailContent}-like base class for change log tables.
* Accounts that upsync message changes require a change log to track local changes between upsyncs.
* A single instance of this class (or subclass) represents one change to upsync to the server.
* This object may actually correspond to multiple rows in the table.
* This class (and subclasses) also contains constants for the table columns and values stored in
* the DB. The base class contains the ones common to all change logs.
*/
public abstract class MessageChangeLogTable {
// DB columns. Note that this class (and subclasses) use some denormalized columns
// (e.g. accountKey) for simplicity at query time and debugging ease.
/** Column name for the row key; this is an autoincrement key. */
public static final String ID = "_id";
/** Column name for a foreign key into Message for the message that's moving. */
public static final String MESSAGE_KEY = "messageKey";
/** Column name for the server-side id for messageKey. */
public static final String SERVER_ID = "messageServerId";
/** Column name for a foreign key into Account for the message that's moving. */
public static final String ACCOUNT_KEY = "accountKey";
/** Column name for a status value indicating where we are with processing this move request. */
public static final String STATUS = "status";
// Status values.
/** Status value indicating this move has not yet been unpsynced. */
public static final int STATUS_NONE = 0;
public static final String STATUS_NONE_STRING = String.valueOf(STATUS_NONE);
/** Status value indicating this move is being upsynced right now. */
public static final int STATUS_PROCESSING = 1;
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;
/** Selection string for querying this table. */
private static final String SELECTION_BY_ACCOUNT_KEY_AND_STATUS =
ACCOUNT_KEY + "=? and " + STATUS + "=?";
/** Selection string prefix for deleting moves for a set of messages. */
private static final String SELECTION_BY_MESSAGE_KEYS_PREFIX = MESSAGE_KEY + " in (";
protected final long mMessageKey;
protected final String mServerId;
protected long mLastId;
protected MessageChangeLogTable(final long messageKey, final String serverId, final long id) {
mMessageKey = messageKey;
mServerId = serverId;
mLastId = id;
}
public final long getMessageId() {
return mMessageKey;
}
public final String getServerId() {
return mServerId;
}
/**
* Update status of all change entries for an account:
* - {@link #STATUS_NONE} -> {@link #STATUS_PROCESSING}
* - {@link #STATUS_PROCESSING} -> {@link #STATUS_FAILED}
* @param cr A {@link ContentResolver}.
* @param uri The content uri for this table.
* @param accountId The account we want to update.
* @return The number of change entries that are now in {@link #STATUS_PROCESSING}.
*/
private static int startProcessing(final ContentResolver cr, final Uri uri,
final String accountId) {
final String[] args = new String[2];
args[0] = accountId;
final ContentValues cv = new ContentValues(1);
// First mark anything that's still processing as failed.
args[1] = STATUS_PROCESSING_STRING;
cv.put(STATUS, STATUS_FAILED);
cr.update(uri, cv, SELECTION_BY_ACCOUNT_KEY_AND_STATUS, args);
// Now mark all unprocessed messages as processing.
args[1] = STATUS_NONE_STRING;
cv.put(STATUS, STATUS_PROCESSING);
return cr.update(uri, cv, SELECTION_BY_ACCOUNT_KEY_AND_STATUS, args);
}
/**
* Query for all move records that are in {@link #STATUS_PROCESSING}.
* Note that this function assumes the underlying table uses an autoincrement id key: it assumes
* that ascending id is the same as chronological order.
* @param cr A {@link ContentResolver}.
* @param uri The content uri for this table.
* @param projection The projection to use for this query.
* @param accountId The account we want to update.
* @return A {@link android.database.Cursor} containing all rows, in id order.
*/
private static Cursor getRowsToProcess(final ContentResolver cr, final Uri uri,
final String[] projection, final String accountId) {
final String[] args = { accountId, STATUS_PROCESSING_STRING };
return cr.query(uri, projection, SELECTION_BY_ACCOUNT_KEY_AND_STATUS, args, ID + " ASC");
}
/**
* Create a selection string for all messages in a set.
* @param messageKeys The set of messages we're interested in.
* @param count The number of messages we're interested in.
* @return The selection string for these messages.
*/
private static String getSelectionForMessages(final long[] messageKeys, final int count) {
final StringBuilder sb = new StringBuilder(SELECTION_BY_MESSAGE_KEYS_PREFIX);
for (int i = 0; i < count; ++i) {
if (i != 0) {
sb.append(",");
}
sb.append(messageKeys[i]);
}
sb.append(")");
return sb.toString();
}
/**
* Delete all rows for a set of messages. Used to clear no-op changes (i.e. multiple rows for
* a message that reverts it to the original state) and after successful upsync.
* @param cr A {@link ContentResolver}.
* @param uri The content uri for this table.
* @param messageKeys The messages to clear.
* @param count The number of message keys.
* @return The number of rows deleted from the DB.
*/
protected static int deleteRowsForMessages(final ContentResolver cr, final Uri uri,
final long[] messageKeys, final int count) {
if (count == 0) {
return 0;
}
return cr.delete(uri, getSelectionForMessages(messageKeys, count), null);
}
/**
* Set the status value for a set of messages.
* @param cr A {@link ContentResolver}.
* @param uri The {@link Uri} for the update.
* @param messageKeys The messages to update.
* @param count The number of messageKeys.
* @param status The new status value for the messages.
* @return The number of rows updated.
*/
private static int updateStatusForMessages(final ContentResolver cr, final Uri uri,
final long[] messageKeys, final int count, final int status) {
if (count == 0) {
return 0;
}
final ContentValues cv = new ContentValues(1);
cv.put(STATUS, status);
return cr.update(uri, cv, getSelectionForMessages(messageKeys, count), null);
}
/**
* Set a set of messages to status = retry.
* @param cr A {@link ContentResolver}.
* @param uri The {@link Uri} for the update.
* @param messageKeys The messages to update.
* @param count The number of messageKeys.
* @return The number of rows updated.
*/
protected static int retryMessages(final ContentResolver cr, final Uri uri,
final long[] messageKeys, final int count) {
return updateStatusForMessages(cr, uri, messageKeys, count, STATUS_NONE);
}
/**
* Set a set of messages to status = failed.
* @param cr A {@link ContentResolver}.
* @param uri The {@link Uri} for the update.
* @param messageKeys The messages to update.
* @param count The number of messageKeys.
* @return The number of rows updated.
*/
protected static int failMessages(final ContentResolver cr, final Uri uri,
final long[] messageKeys, final int count) {
return updateStatusForMessages(cr, uri, messageKeys, count, STATUS_FAILED);
}
/**
* Start processing our table and get a {@link Cursor} for the rows to process.
* @param cr A {@link ContentResolver}.
* @param uri The {@link Uri} for the update.
* @param projection The projection to use for our read.
* @param accountId The account we're interested in.
* @return A {@link Cursor} with the change log rows we're interested in.
*/
protected static Cursor getCursor(final ContentResolver cr, final Uri uri,
final String[] projection, final long accountId) {
final String accountIdString = String.valueOf(accountId);
if (startProcessing(cr, uri, accountIdString) <= 0) {
return null;
}
return getRowsToProcess(cr, uri, projection, accountIdString);
}
}

View File

@ -0,0 +1,202 @@
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 MessageMove table.
*/
public class MessageMove extends MessageChangeLogTable {
/** Logging tag. */
public static final String LOG_TAG = "MessageMove";
/** The name for this table in the database. */
public static final String TABLE_NAME = "MessageMove";
/** The path for the URI for interacting with message moves. */
public static final String PATH = "messageMove";
/** The URI for dealing with message move data. */
public static Uri CONTENT_URI;
// DB columns.
/** Column name for a foreign key into Mailbox for the folder the message is moving from. */
public static final String SRC_FOLDER_KEY = "srcFolderKey";
/** Column name for a foreign key into Mailbox for the folder the message is moving to. */
public static final String DST_FOLDER_KEY = "dstFolderKey";
/** Column name for the server-side id for srcFolderKey. */
public static final String SRC_FOLDER_SERVER_ID = "srcFolderServerId";
/** Column name for the server-side id for dstFolderKey. */
public static final String DST_FOLDER_SERVER_ID = "dstFolderServerId";
/**
* Projection for a query to get all columns necessary for an actual move.
*/
private static final class ProjectionMoveQuery {
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_SRC_FOLDER_KEY = 3;
public static final int COLUMN_DST_FOLDER_KEY = 4;
public static final int COLUMN_SRC_FOLDER_SERVER_ID = 5;
public static final int COLUMN_DST_FOLDER_SERVER_ID = 6;
public static final String[] PROJECTION = new String[] {
ID, MESSAGE_KEY, SERVER_ID,
SRC_FOLDER_KEY, DST_FOLDER_KEY,
SRC_FOLDER_SERVER_ID, DST_FOLDER_SERVER_ID
};
}
// The actual fields.
private final long mSrcFolderKey;
private long mDstFolderKey;
private final String mSrcFolderServerId;
private String mDstFolderServerId;
private MessageMove(final long messageKey,final String serverId, final long id,
final long srcFolderKey, final long dstFolderKey,
final String srcFolderServerId, final String dstFolderServerId) {
super(messageKey, serverId, id);
mSrcFolderKey = srcFolderKey;
mDstFolderKey = dstFolderKey;
mSrcFolderServerId = srcFolderServerId;
mDstFolderServerId = dstFolderServerId;
}
public final long getSourceFolderKey() {
return mSrcFolderKey;
}
public final String getSourceFolderId() {
return mSrcFolderServerId;
}
public final String getDestFolderId() {
return mDstFolderServerId;
}
/**
* Initialize static state for this class.
*/
public static void init() {
CONTENT_URI = EmailContent.CONTENT_URI.buildUpon().appendEncodedPath(PATH).build();
}
/**
* Get the final moves that we want 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 pending moves results in a no-op (i.e. the message has been moved
* back to its original folder) have their moves cleared from the DB without any upsync.
* @param context A {@link Context}.
* @param accountId The account we want to update.
* @return The final moves to send to the server, or null if there are none.
*/
public static List<MessageMove> getMoves(final Context context, final long accountId) {
final ContentResolver cr = context.getContentResolver();
final Cursor c = getCursor(cr, CONTENT_URI, ProjectionMoveQuery.PROJECTION, accountId);
if (c == null) {
return null;
}
// Collapse any rows in the cursor that are acting on the same message. We know the cursor
// returned by getRowsToProcess is ordered from oldest to newest, and we use this fact to
// get the original and final folder for the message.
LongSparseArray<MessageMove> movesMap = new LongSparseArray();
try {
while (c.moveToNext()) {
final long id = c.getLong(ProjectionMoveQuery.COLUMN_ID);
final long messageKey = c.getLong(ProjectionMoveQuery.COLUMN_MESSAGE_KEY);
final String serverId = c.getString(ProjectionMoveQuery.COLUMN_SERVER_ID);
final long srcFolderKey = c.getLong(ProjectionMoveQuery.COLUMN_SRC_FOLDER_KEY);
final long dstFolderKey = c.getLong(ProjectionMoveQuery.COLUMN_DST_FOLDER_KEY);
final String srcFolderServerId =
c.getString(ProjectionMoveQuery.COLUMN_SRC_FOLDER_SERVER_ID);
final String dstFolderServerId =
c.getString(ProjectionMoveQuery.COLUMN_DST_FOLDER_SERVER_ID);
final MessageMove existingMove = movesMap.get(messageKey);
if (existingMove != null) {
if (existingMove.mLastId >= id) {
LogUtils.w(LOG_TAG, "Moves were not in ascending id order");
}
if (!existingMove.mDstFolderServerId.equals(srcFolderServerId) ||
existingMove.mDstFolderKey != srcFolderKey) {
LogUtils.w(LOG_TAG, "existing move's dst not same as this move's src");
}
existingMove.mDstFolderKey = dstFolderKey;
existingMove.mDstFolderServerId = dstFolderServerId;
existingMove.mLastId = id;
} else {
movesMap.put(messageKey, new MessageMove(messageKey, serverId, id,
srcFolderKey, dstFolderKey, srcFolderServerId, dstFolderServerId));
}
}
} finally {
c.close();
}
// Prune any no-op moves (i.e. messages that have been moved back to the initial folder).
final int moveCount = movesMap.size();
final long[] unmovedMessages = new long[moveCount];
int unmovedMessagesCount = 0;
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) {
unmovedMessages[unmovedMessagesCount] = move.mMessageKey;
++unmovedMessagesCount;
} else {
moves.add(move);
}
}
if (unmovedMessagesCount != 0) {
deleteRowsForMessages(cr, CONTENT_URI, unmovedMessages, unmovedMessagesCount);
}
if (moves.isEmpty()) {
return null;
}
return moves;
}
/**
* 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);
}
/**
* Clean up the table to reflect upsyncs that failed and need to be reverted.
* @param cr A {@link ContentResolver}
* @param messageKeys The messages to update.
* @param count The number of messages.
*/
public static void upsyncFail(final ContentResolver cr, final long[] messageKeys,
final int count) {
failMessages(cr, CONTENT_URI, messageKeys, count);
}
}

View File

@ -0,0 +1,155 @@
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 static final class 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 MessageStateChange(final long messageKey,final String serverId, final long id,
final int oldFlagRead, final int newFlagRead,
final int oldFlagFavorite, final int newFlagFavorite) {
super(messageKey, serverId, id);
mOldFlagRead = oldFlagRead;
mNewFlagRead = newFlagRead;
mOldFlagFavorite = oldFlagFavorite;
mNewFlagFavorite = newFlagFavorite;
}
/**
* Initialize static state for this class.
*/
public static void init() {
CONTENT_URI = EmailContent.CONTENT_URI.buildUpon().appendEncodedPath(PATH).build();
}
public static List<MessageStateChange> getChanges(final Context context, final long accountId) {
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 = (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 {
changesMap.put(messageKey, new MessageStateChange(messageKey, serverId, id,
oldFlagRead, newFlagRead, oldFlagFavorite, newFlagFavorite));
}
}
} 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);
if (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;
}
}

View File

@ -47,6 +47,9 @@ import com.android.emailcommon.provider.EmailContent.QuickResponseColumns;
import com.android.emailcommon.provider.EmailContent.SyncColumns;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.provider.MessageChangeLogTable;
import com.android.emailcommon.provider.MessageMove;
import com.android.emailcommon.provider.MessageStateChange;
import com.android.emailcommon.provider.Policy;
import com.android.emailcommon.provider.QuickResponse;
import com.android.emailcommon.service.LegacyPolicySet;
@ -148,8 +151,9 @@ public final class DBHelper {
// Version 113: Restore message_count to being useful.
// Version 114: Add lastFullSyncTime column
// Version 115: Add pingDuration column
// Version 116: Add MessageMove & MessageStateChange tables.
public static final int DATABASE_VERSION = 115;
public static final int DATABASE_VERSION = 116;
// Any changes to the database format *must* include update-in-place code.
// Original version: 2
@ -325,6 +329,79 @@ public final class DBHelper {
createMessageTable(db);
}
/**
* Common columns for all {@link MessageChangeLogTable} tables.
*/
private static String MESSAGE_CHANGE_LOG_COLUMNS =
MessageChangeLogTable.ID + " integer primary key autoincrement, "
+ MessageChangeLogTable.MESSAGE_KEY + " integer, "
+ MessageChangeLogTable.SERVER_ID + " text, "
+ MessageChangeLogTable.ACCOUNT_KEY + " integer, "
+ MessageChangeLogTable.STATUS + " integer, ";
/**
* Create indices common to all {@link MessageChangeLogTable} tables.
* @param db The {@link SQLiteDatabase}.
* @param tableName The name of this particular table.
*/
private static void createMessageChangeLogTableIndices(final SQLiteDatabase db,
final String tableName) {
db.execSQL(createIndex(tableName, MessageChangeLogTable.MESSAGE_KEY));
db.execSQL(createIndex(tableName, MessageChangeLogTable.ACCOUNT_KEY));
}
/**
* Create triggers common to all {@link MessageChangeLogTable} tables.
* @param db The {@link SQLiteDatabase}.
* @param tableName The name of this particular table.
*/
private static void createMessageChangeLogTableTriggers(final SQLiteDatabase db,
final String tableName) {
// Trigger to delete from the change log when a message is deleted.
db.execSQL("create trigger " + tableName + "_delete_message before delete on "
+ Message.TABLE_NAME + " for each row begin delete from " + tableName
+ " where " + MessageChangeLogTable.MESSAGE_KEY + "=old." + MessageColumns.ID
+ "; end");
// Trigger to delete from the change log when an account is deleted.
db.execSQL("create trigger " + tableName + "_delete_account before delete on "
+ Account.TABLE_NAME + " for each row begin delete from " + tableName
+ " where " + MessageChangeLogTable.ACCOUNT_KEY + "=old." + AccountColumns.ID
+ "; end");
}
/**
* Create the MessageMove table.
* @param db The {@link SQLiteDatabase}.
*/
private static void createMessageMoveTable(final SQLiteDatabase db) {
db.execSQL("create table " + MessageMove.TABLE_NAME + " ("
+ MESSAGE_CHANGE_LOG_COLUMNS
+ MessageMove.SRC_FOLDER_KEY + " integer, "
+ MessageMove.DST_FOLDER_KEY + " integer, "
+ MessageMove.SRC_FOLDER_SERVER_ID + " text, "
+ MessageMove.DST_FOLDER_SERVER_ID + " text);");
createMessageChangeLogTableIndices(db, MessageMove.TABLE_NAME);
createMessageChangeLogTableTriggers(db, MessageMove.TABLE_NAME);
}
/**
* Create the MessageStateChange table.
* @param db The {@link SQLiteDatabase}.
*/
private static void createMessageStateChangeTable(final SQLiteDatabase db) {
db.execSQL("create table " + MessageStateChange.TABLE_NAME + " ("
+ MESSAGE_CHANGE_LOG_COLUMNS
+ MessageStateChange.OLD_FLAG_READ + " integer, "
+ MessageStateChange.NEW_FLAG_READ + " integer, "
+ MessageStateChange.OLD_FLAG_FAVORITE + " integer, "
+ MessageStateChange.NEW_FLAG_FAVORITE + " integer);");
createMessageChangeLogTableIndices(db, MessageStateChange.TABLE_NAME);
createMessageChangeLogTableTriggers(db, MessageStateChange.TABLE_NAME);
}
@SuppressWarnings("deprecation")
static void createAccountTable(SQLiteDatabase db) {
String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
@ -594,6 +671,8 @@ public final class DBHelper {
createMailboxTable(db);
createHostAuthTable(db);
createAccountTable(db);
createMessageMoveTable(db);
createMessageStateChangeTable(db);
createPolicyTable(db);
createQuickResponseTable(db);
}
@ -1087,6 +1166,11 @@ public final class DBHelper {
LogUtils.w(TAG, "Exception upgrading EmailProvider.db from v113 to v114", e);
}
}
if (oldVersion <= 115) {
createMessageMoveTable(db);
createMessageStateChangeTable(db);
}
}
@Override

View File

@ -71,11 +71,13 @@ import com.android.emailcommon.provider.EmailContent.BodyColumns;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.EmailContent.MessageColumns;
import com.android.emailcommon.provider.EmailContent.MovedMessagesColumns;
import com.android.emailcommon.provider.EmailContent.PolicyColumns;
import com.android.emailcommon.provider.EmailContent.SyncColumns;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.provider.MessageChangeLogTable;
import com.android.emailcommon.provider.MessageMove;
import com.android.emailcommon.provider.MessageStateChange;
import com.android.emailcommon.provider.Policy;
import com.android.emailcommon.provider.QuickResponse;
import com.android.emailcommon.service.EmailServiceProxy;
@ -114,6 +116,7 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
@ -184,7 +187,8 @@ public class EmailProvider extends ContentProvider {
private static final int MESSAGE_ID = MESSAGE_BASE + 1;
private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2;
private static final int MESSAGE_SELECTION = MESSAGE_BASE + 3;
private static final int MOVED_MESSAGES = MESSAGE_BASE + 4;
private static final int MESSAGE_MOVE = MESSAGE_BASE + 4;
private static final int MESSAGE_STATE_CHANGE = MESSAGE_BASE + 5;
private static final int ATTACHMENT_BASE = 0x3000;
private static final int ATTACHMENT = ATTACHMENT_BASE;
@ -303,43 +307,6 @@ public class EmailProvider extends ContentProvider {
private static final String SYNC_STATUS_CALLBACK_METHOD = "sync_status";
/** FROM clause for a SQL query to get info about updated messages. */
private static final String GET_CHANGED_MESSAGES_FROM_CLAUSE = "from "
+ Message.UPDATED_TABLE_NAME + " as u inner join " + Message.TABLE_NAME
+ " as m using (" + MessageColumns.ID + ")";
/** FROM clause for a SQL query to get moved messages info. */
private static final String GET_MOVED_MESSAGES_FROM_CLAUSE = GET_CHANGED_MESSAGES_FROM_CLAUSE
+ " inner join " + Mailbox.TABLE_NAME + " as src on u." + MessageColumns.MAILBOX_KEY
+ "=src." + MailboxColumns.ID
+ " inner join " + Mailbox.TABLE_NAME + " as dst on m." + MessageColumns.MAILBOX_KEY
+ "=dst." + MailboxColumns.ID;
/** WHERE clause for a SQL query to get info about updated messages. */
private static final String GET_CHANGED_MESSAGES_WHERE_CLAUSE =
"where u." + MessageColumns.ACCOUNT_KEY + "=?";
/** WHERE clause for a SQL query to get moved messages info. */
private static final String GET_MOVED_MESSAGES_WHERE_CLAUSE = GET_CHANGED_MESSAGES_WHERE_CLAUSE
+ " and src." + MailboxColumns.ID + "!=dst." + MailboxColumns.ID;
/** SELECT clause for a SQL query to get moved messages info. */
private static final String GET_MOVED_MESSAGES_SELECTION = "select "
+ "u." + SyncColumns.SERVER_ID + " as " + MovedMessagesColumns.MESSAGE_ID
+ ",u." + MessageColumns.ID + " as " + MovedMessagesColumns.LOCAL_MESSAGE_ID
+ ",src." + MailboxColumns.SERVER_ID + " as " + MovedMessagesColumns.SOURCE_FOLDER_ID
+ ",src." + MailboxColumns.ID + " as " + MovedMessagesColumns.LOCAL_SOURCE_FOLDER_ID
+ ",dst." + MailboxColumns.SERVER_ID + " as " + MovedMessagesColumns.DEST_FOLDER_ID
+ ",dst." + MailboxColumns.ID + " as " + MovedMessagesColumns.LOCAL_DEST_FOLDER_ID;
/**
* A raw SQL query for getting the moved messages info.
* See {@link EmailContent#MOVED_MESSAGES_URI} for details on this query.
*/
private static final String GET_MOVED_MESSAGES_QUERY = GET_MOVED_MESSAGES_SELECTION + " "
+ GET_MOVED_MESSAGES_FROM_CLAUSE + " " + GET_MOVED_MESSAGES_WHERE_CLAUSE;
/**
* Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in
* @param uri the Uri to match
@ -656,7 +623,12 @@ public class EmailProvider extends ContentProvider {
case POLICY:
result = db.delete(tableName, selection, selectionArgs);
break;
case MESSAGE_MOVE:
db.delete(MessageMove.TABLE_NAME, selection, selectionArgs);
break;
case MESSAGE_STATE_CHANGE:
db.delete(MessageStateChange.TABLE_NAME, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
@ -967,8 +939,9 @@ public class EmailProvider extends ContentProvider {
sURIMatcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID);
sURIMatcher.addURI(EmailContent.AUTHORITY, "messageBySelection", MESSAGE_SELECTION);
sURIMatcher.addURI(EmailContent.AUTHORITY, EmailContent.MOVED_MESSAGES_PATH + "/#",
MOVED_MESSAGES);
sURIMatcher.addURI(EmailContent.AUTHORITY, MessageMove.PATH, MESSAGE_MOVE);
sURIMatcher.addURI(EmailContent.AUTHORITY, MessageStateChange.PATH,
MESSAGE_STATE_CHANGE);
/**
* THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY
@ -1153,8 +1126,12 @@ public class EmailProvider extends ContentProvider {
case MAILBOX_MESSAGE_COUNT:
c = getMailboxMessageCount(uri);
return c;
case MOVED_MESSAGES:
return getMovedMessages(uri.getPathSegments().get(1));
case MESSAGE_MOVE:
return db.query(MessageMove.TABLE_NAME, projection, selection, selectionArgs,
null, null, sortOrder, limit);
case MESSAGE_STATE_CHANGE:
return db.query(MessageStateChange.TABLE_NAME, projection, selection,
selectionArgs, null, null, sortOrder, limit);
case BODY:
case MESSAGE:
case UPDATED_MESSAGE:
@ -1226,18 +1203,6 @@ public class EmailProvider extends ContentProvider {
return c;
}
/**
* Queries the DB for the moved messages info for an account. See the comments for
* {@link EmailContent#MOVED_MESSAGES_URI} for details on what this should return.
* TODO: May be convenient to eventually allow a projection, or selection args, etc.
* @param accountId The account to query.
* @return The moved messages info cursor.
*/
private Cursor getMovedMessages(final String accountId) {
return getDatabase(getContext()).rawQuery(GET_MOVED_MESSAGES_QUERY,
new String[] { accountId });
}
private static String whereWithId(String id, String selection) {
StringBuilder sb = new StringBuilder(256);
sb.append("_id=");
@ -1427,6 +1392,65 @@ public class EmailProvider extends ContentProvider {
}
}
private static final String MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX = "insert into %s ("
+ MessageChangeLogTable.MESSAGE_KEY + "," + MessageChangeLogTable.SERVER_ID + ","
+ MessageChangeLogTable.ACCOUNT_KEY + "," + MessageChangeLogTable.STATUS + ",";
private static final String MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX = ") values (%s, "
+ "(select " + Message.SERVER_ID + " from " + Message.TABLE_NAME + " where _id=%s),"
+ "(select " + Message.ACCOUNT_KEY + " from " + Message.TABLE_NAME + " where _id=%s),"
+ MessageMove.STATUS_NONE_STRING + ",";
/**
* Formatting string to generate the SQL statement for inserting into MessageMove.
* The formatting parameters are:
* table name, message id x 4, destination folder id, message id, destination folder id.
* Duplications are needed for sub-selects.
*/
private static final String MESSAGE_MOVE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX
+ MessageMove.SRC_FOLDER_KEY + "," + MessageMove.DST_FOLDER_KEY + ","
+ MessageMove.SRC_FOLDER_SERVER_ID + "," + MessageMove.DST_FOLDER_SERVER_ID
+ MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX
+ "(select " + Message.MAILBOX_KEY + " from " + Message.TABLE_NAME + " where _id=%s),"
+ "%d,"
+ "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=(select "
+ Message.MAILBOX_KEY + " from " + Message.TABLE_NAME + " where _id=%s)),"
+ "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=%d))";
/**
* Insert a row into the MessageMove table when that message is moved.
* @param db The {@link SQLiteDatabase}.
* @param messageId The id of the message being moved.
* @param dstFolderKey The folder to which the message is being moved.
*/
private void addToMessageMove(final SQLiteDatabase db, final String messageId,
final long dstFolderKey) {
db.execSQL(String.format(Locale.US, MESSAGE_MOVE_INSERT, MessageMove.TABLE_NAME,
messageId, messageId, messageId, messageId, dstFolderKey, messageId, dstFolderKey));
}
/**
* Formatting string to generate the SQL statement for inserting into MessageStateChange.
* The formatting parameters are:
* table name, message id x 4, new flag read, message id, new flag favorite.
* Duplications are needed for sub-selects.
*/
private static final String MESSAGE_STATE_CHANGE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX
+ MessageStateChange.OLD_FLAG_READ + "," + MessageStateChange.NEW_FLAG_READ + ","
+ MessageStateChange.OLD_FLAG_FAVORITE + "," + MessageStateChange.NEW_FLAG_FAVORITE
+ MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX
+ "(select " + Message.FLAG_READ + " from " + Message.TABLE_NAME + " where _id=%s),"
+ "%d,"
+ "(select " + Message.FLAG_FAVORITE + " from " + Message.TABLE_NAME + " where _id=%s),"
+ "%d)";
private void addToMessageStateChange(final SQLiteDatabase db, final String messageId,
final int newFlagRead, final int newFlagFavorite) {
db.execSQL(String.format(Locale.US, MESSAGE_STATE_CHANGE_INSERT,
MessageStateChange.TABLE_NAME, messageId, messageId, messageId, messageId,
newFlagRead, messageId, newFlagFavorite));
}
// select count(*) from (select count(*) as dupes from Mailbox where accountKey=?
// group by serverId) where dupes > 1;
private static final String ACCOUNT_INTEGRITY_SQL =
@ -1529,6 +1553,20 @@ public class EmailProvider extends ContentProvider {
// 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);
}
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 if (match == MESSAGE_ID) {
db.execSQL(UPDATED_MESSAGE_DELETE + id);
}
@ -1594,6 +1632,13 @@ public class EmailProvider extends ContentProvider {
// Affects all accounts. Just invalidate all account cache.
notificationUri = Account.CONTENT_URI; // Only notify account cursors.
break;
case MESSAGE_MOVE:
result = db.update(MessageMove.TABLE_NAME, values, selection, selectionArgs);
break;
case MESSAGE_STATE_CHANGE:
result = db.update(MessageStateChange.TABLE_NAME, values, selection,
selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}