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 androidx.collection.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 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)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 getNewFlagRead()81 public final int getNewFlagRead() { 82 if (mOldFlagRead == mNewFlagRead) { 83 return VALUE_UNCHANGED; 84 } 85 return mNewFlagRead; 86 } 87 getNewFlagFavorite()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 */ init()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 */ getChanges(final Context context, final long accountId, final boolean ignoreFavorites)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 */ convertToChangesMap( final List<MessageStateChange> changes)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 */ upsyncSuccessful(final ContentResolver cr, final long[] messageKeys, final int count)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 */ upsyncRetry(final ContentResolver cr, final long[] messageKeys, final int count)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