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 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