diff --git a/src/com/android/email/mail/Flag.java b/src/com/android/email/mail/Flag.java index 4817af613..4a46aa290 100644 --- a/src/com/android/email/mail/Flag.java +++ b/src/com/android/email/mail/Flag.java @@ -20,6 +20,9 @@ package com.android.email.mail; * Flags that can be applied to Messages. */ public enum Flag { + + // If adding new flags: ALL FLAGS MUST BE UPPER CASE. + DELETED, SEEN, ANSWERED, @@ -73,16 +76,4 @@ public enum Flag { */ X_STORE_2, - /** - * General purpose flag that can be used by any remote store. The flag will be - * saved and restored by the LocalStore. - */ - X_STORE_3, - - /** - * General purpose flag that can be used by any remote store. The flag will be - * saved and restored by the LocalStore. - */ - X_STORE_4, - } diff --git a/src/com/android/email/mail/Folder.java b/src/com/android/email/mail/Folder.java index 12adecb48..eef70b644 100644 --- a/src/com/android/email/mail/Folder.java +++ b/src/com/android/email/mail/Folder.java @@ -104,6 +104,21 @@ public abstract class Folder { public abstract Message[] getMessages(String[] uids, MessageRetrievalListener listener) throws MessagingException; + + /** + * Return a set of messages based on the state of the flags. + * Note: Not typically implemented in remote stores, so not abstract. + * + * @param setFlags The flags that should be set for a message to be selected (can be null) + * @param clearFlags The flags that should be clear for a message to be selected (can be null) + * @param listener + * @return A list of messages matching the desired flag states. + * @throws MessagingException + */ + public Message[] getMessages(Flag[] setFlags, Flag[] clearFlags, + MessageRetrievalListener listener) throws MessagingException { + throw new MessagingException("Not implemented"); + } public abstract void appendMessages(Message[] messages) throws MessagingException; @@ -157,6 +172,18 @@ public abstract class Folder { * @return the data saved by the Folder, or defaultValue if never set. */ public String getPersistentString(String key, String defaultValue); + + /** + * In a single transaction: Set a key/value pair for the folder, and bulk set or clear + * message flags. Typically used at the beginning or conclusion of a bulk sync operation. + * + * @param key if non-null, the transaction will set this folder persistent value + * @param value the value that will be stored for the key + * @param setFlags if non-null, flag(s) will be set for all messages in the folder + * @param clearFlags if non-null, flag(s) will be cleared for all messages in the folder + */ + public void setPersistentStringAndMessageFlags(String key, String value, + Flag[] setFlags, Flag[] clearFlags) throws MessagingException; } @Override diff --git a/src/com/android/email/mail/Message.java b/src/com/android/email/mail/Message.java index 2f4b812a2..b52e6c97d 100644 --- a/src/com/android/email/mail/Message.java +++ b/src/com/android/email/mail/Message.java @@ -130,4 +130,9 @@ public abstract class Message implements Part, Body { } public abstract void saveChanges() throws MessagingException; + + @Override + public String toString() { + return getClass().getSimpleName() + ':' + mUid; + } } diff --git a/src/com/android/email/mail/store/LocalStore.java b/src/com/android/email/mail/store/LocalStore.java index cfc9d8179..9212025bf 100644 --- a/src/com/android/email/mail/store/LocalStore.java +++ b/src/com/android/email/mail/store/LocalStore.java @@ -78,9 +78,10 @@ public class LocalStore extends Store { * 19 - Added message_id column to messages table. * 20 1.5 Added content_id column to attachments table. * 21 - Added remote_store_data table + * 22 - Added store_flag_1 and store_flag_2 columns to messages table. */ - private static final int DB_VERSION = 21; + private static final int DB_VERSION = 22; private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.X_DESTROYED, Flag.SEEN }; @@ -142,7 +143,8 @@ public class LocalStore extends Store { "uid TEXT, subject TEXT, date INTEGER, flags TEXT, sender_list TEXT, " + "to_list TEXT, cc_list TEXT, bcc_list TEXT, reply_to_list TEXT, " + "html_content TEXT, text_content TEXT, attachment_count INTEGER, " + - "internal_date INTEGER, message_id TEXT)"); + "internal_date INTEGER, message_id TEXT, store_flag_1 INTEGER, " + + "store_flag_2 INTEGER)"); mDb.execSQL("DROP TABLE IF EXISTS attachments"); mDb.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER," @@ -184,6 +186,14 @@ public class LocalStore extends Store { addFolderDeleteTrigger(); mDb.setVersion(21); } + if (oldVersion < 22) { + /** + * Upgrade 21 to 22: add store_flag_1 and store_flag_2 to messages table + */ + mDb.execSQL("ALTER TABLE messages ADD COLUMN store_flag_1 INTEGER;"); + mDb.execSQL("ALTER TABLE messages ADD COLUMN store_flag_2 INTEGER;"); + mDb.setVersion(22); + } } if (mDb.getVersion() != DB_VERSION) { @@ -201,9 +211,10 @@ public class LocalStore extends Store { */ private void addRemoteStoreDataTable() { mDb.execSQL("DROP TABLE IF EXISTS remote_store_data"); - mDb.execSQL("CREATE TABLE remote_store_data " - + "(id INTEGER PRIMARY KEY, folder_id INTEGER, " - + "data_key TEXT, data TEXT)"); + mDb.execSQL("CREATE TABLE remote_store_data (" + + "id INTEGER PRIMARY KEY, folder_id INTEGER, data_key TEXT, data TEXT, " + + "UNIQUE (folder_id, data_key) ON CONFLICT REPLACE" + + ")"); } /** @@ -648,13 +659,21 @@ public class LocalStore extends Store { } /** - * Populate a message from a cursor with the following colummns: + * The columns to select when calling populateMessageFromGetMessageCursor() + */ + private final String POPULATE_MESSAGE_SELECT_COLUMNS = + "subject, sender_list, date, uid, flags, id, to_list, cc_list, " + + "bcc_list, reply_to_list, attachment_count, internal_date, message_id, " + + "store_flag_1, store_flag_2"; + + /** + * Populate a message from a cursor with the following columns: * * 0 subject * 1 from address * 2 date (long) * 3 uid - * 4 flag list + * 4 flag list (older flags - comma-separated string) * 5 local id (long) * 6 to addresses * 7 cc addresses @@ -663,6 +682,8 @@ public class LocalStore extends Store { * 10 attachment count (int) * 11 internal date (long) * 12 message id (from Mime headers) + * 13 store flag 1 + * 14 store flag 2 */ private void populateMessageFromGetMessageCursor(LocalMessage message, Cursor cursor) throws MessagingException{ @@ -691,6 +712,8 @@ public class LocalStore extends Store { message.mAttachmentCount = cursor.getInt(10); message.setInternalDate(new Date(cursor.getLong(11))); message.setMessageId(cursor.getString(12)); + message.setFlagInternal(Flag.X_STORE_1, (0 != cursor.getInt(13))); + message.setFlagInternal(Flag.X_STORE_2, (0 != cursor.getInt(14))); } @Override @@ -708,9 +731,9 @@ public class LocalStore extends Store { Cursor cursor = null; try { cursor = mDb.rawQuery( - "SELECT subject, sender_list, date, uid, flags, id, to_list, cc_list, " - + "bcc_list, reply_to_list, attachment_count, internal_date, message_id " - + "FROM messages " + "WHERE uid = ? " + "AND folder_id = ?", + "SELECT " + POPULATE_MESSAGE_SELECT_COLUMNS + + " FROM messages" + + " WHERE uid = ? AND folder_id = ?", new String[] { message.getUid(), Long.toString(mFolderId) }); @@ -734,10 +757,11 @@ public class LocalStore extends Store { Cursor cursor = null; try { cursor = mDb.rawQuery( - "SELECT subject, sender_list, date, uid, flags, id, to_list, cc_list, " - + "bcc_list, reply_to_list, attachment_count, internal_date, message_id " - + "FROM messages " + "WHERE folder_id = ?", new String[] { - Long.toString(mFolderId) + "SELECT " + POPULATE_MESSAGE_SELECT_COLUMNS + + " FROM messages" + + " WHERE folder_id = ?", + new String[] { + Long.toString(mFolderId) }); while (cursor.moveToNext()) { @@ -768,6 +792,73 @@ public class LocalStore extends Store { } return messages.toArray(new Message[] {}); } + + /** + * Return a set of messages based on the state of the flags. + * + * @param setFlags The flags that should be set for a message to be selected (null ok) + * @param clearFlags The flags that should be clear for a message to be selected (null ok) + * @param listener + * @return A list of messages matching the desired flag states. + * @throws MessagingException + */ + @Override + public Message[] getMessages(Flag[] setFlags, Flag[] clearFlags, + MessageRetrievalListener listener) throws MessagingException { + // Generate WHERE clause based on flags observed + StringBuilder sql = new StringBuilder( + "SELECT " + POPULATE_MESSAGE_SELECT_COLUMNS + + " FROM messages" + + " WHERE "); + if (setFlags != null) { + for (Flag flag : setFlags) { + if (flag == Flag.X_STORE_1) { + sql.append("store_flag_1 = 1 AND "); + } else if (flag == Flag.X_STORE_2) { + sql.append("store_flag_2 = 1 AND "); + } else { + throw new MessagingException("Unsupported flag " + flag); + } + } + } + if (clearFlags != null) { + for (Flag flag : clearFlags) { + if (flag == Flag.X_STORE_1) { + sql.append("store_flag_1 = 0 AND "); + } else if (flag == Flag.X_STORE_2) { + sql.append("store_flag_2 = 0 AND "); + } else { + throw new MessagingException("Unsupported flag " + flag); + } + } + } + sql.append("folder_id = ?"); + + open(OpenMode.READ_WRITE); + ArrayList messages = new ArrayList(); + + Cursor cursor = null; + try { + cursor = mDb.rawQuery( + sql.toString(), + new String[] { + Long.toString(mFolderId) + }); + + while (cursor.moveToNext()) { + LocalMessage message = new LocalMessage(null, this); + populateMessageFromGetMessageCursor(message, cursor); + messages.add(message); + } + } + finally { + if (cursor != null) { + cursor.close(); + } + } + + return messages.toArray(new Message[] {}); + } @Override public void copyMessages(Message[] msgs, Folder folder) throws MessagingException { @@ -845,7 +936,7 @@ public class LocalStore extends Store { cv.put("sender_list", Address.pack(message.getFrom())); cv.put("date", message.getSentDate() == null ? System.currentTimeMillis() : message.getSentDate().getTime()); - cv.put("flags", Utility.combine(message.getFlags(), ',').toUpperCase()); + cv.put("flags", makeFlagsString(message)); cv.put("folder_id", mFolderId); cv.put("to_list", Address.pack(message.getRecipients(RecipientType.TO))); cv.put("cc_list", Address.pack(message.getRecipients(RecipientType.CC))); @@ -857,6 +948,8 @@ public class LocalStore extends Store { cv.put("internal_date", message.getInternalDate() == null ? System.currentTimeMillis() : message.getInternalDate().getTime()); cv.put("message_id", ((MimeMessage)message).getMessageId()); + cv.put("store_flag_1", makeFlagNumeric(message, Flag.X_STORE_1)); + cv.put("store_flag_2", makeFlagNumeric(message, Flag.X_STORE_2)); long messageId = mDb.insert("messages", "uid", cv); for (Part attachment : attachments) { saveAttachment(messageId, attachment, copy); @@ -909,7 +1002,9 @@ public class LocalStore extends Store { + "uid = ?, subject = ?, sender_list = ?, date = ?, flags = ?, " + "folder_id = ?, to_list = ?, cc_list = ?, bcc_list = ?, " + "html_content = ?, text_content = ?, reply_to_list = ?, " - + "attachment_count = ?, message_id = ? WHERE id = ?", + + "attachment_count = ?, message_id = ?, store_flag_1 = ?, " + + "store_flag_2 = ? " + + "WHERE id = ?", new Object[] { message.getUid(), message.getSubject(), @@ -917,7 +1012,7 @@ public class LocalStore extends Store { message.getSentDate() == null ? System .currentTimeMillis() : message.getSentDate() .getTime(), - Utility.combine(message.getFlags(), ',').toUpperCase(), + makeFlagsString(message), mFolderId, Address.pack(message .getRecipients(RecipientType.TO)), @@ -930,6 +1025,9 @@ public class LocalStore extends Store { Address.pack(message.getReplyTo()), attachments.size(), message.getMessageId(), + makeFlagNumeric(message, Flag.X_STORE_1), + makeFlagNumeric(message, Flag.X_STORE_2), + message.mId }); @@ -1190,30 +1288,64 @@ public class LocalStore extends Store { } public void setPersistentString(String key, String value) { - // TODO apply sql-foo and replace this with a single statement - Cursor cursor = null; + ContentValues cv = new ContentValues(); + cv.put("folder_id", Long.toString(mFolderId)); + cv.put("data_key", key); + cv.put("data", value); + // Note: Table has on-conflict-replace rule + mDb.insert("remote_store_data", null, cv); + } + + /** + * Transactionally combine a key/value and a complete message flags flip. Used + * for setting sync bits in messages. + * + * @param key + * @param value + * @param setFlags + * @param clearFlags + */ + public void setPersistentStringAndMessageFlags(String key, String value, + Flag[] setFlags, Flag[] clearFlags) throws MessagingException { + mDb.beginTransaction(); try { - final String where = "folder_id = ? AND data_key = ?"; - String[] whereArgs = new String[] { Long.toString(mFolderId), key }; + // take care of folder persistence + if (key != null) { + setPersistentString(key, value); + } + + // take care of flags ContentValues cv = new ContentValues(); - cv.put("data", value); - cursor = mDb.query("remote_store_data", - new String[] { "data" }, - where, whereArgs, - null, null, null); - if (cursor != null && cursor.moveToNext()) { - mDb.update("remote_store_data", cv, where, whereArgs); - } else { - cv.put("folder_id", Long.toString(mFolderId)); - cv.put("data_key", key); - mDb.insert("remote_store_data", null, cv); + if (setFlags != null) { + for (Flag flag : setFlags) { + if (flag == Flag.X_STORE_1) { + cv.put("store_flag_1", 1); + } else if (flag == Flag.X_STORE_2) { + cv.put("store_flag_2", 1); + } else { + throw new MessagingException("Unsupported flag " + flag); + } + } } - } - finally { - if (cursor != null) { - cursor.close(); + if (clearFlags != null) { + for (Flag flag : clearFlags) { + if (flag == Flag.X_STORE_1) { + cv.put("store_flag_1", 0); + } else if (flag == Flag.X_STORE_2) { + cv.put("store_flag_2", 0); + } else { + throw new MessagingException("Unsupported flag " + flag); + } + } } + mDb.update("messages", cv, + "folder_id = ?", new String[] { Long.toString(mFolderId) }); + + mDb.setTransactionSuccessful(); + } finally { + mDb.endTransaction(); } + } } @@ -1309,12 +1441,53 @@ public class LocalStore extends Store { /* * Set the flags on the message. */ - mDb.execSQL("UPDATE messages " + "SET flags = ? " + "WHERE id = ?", new Object[] { - Utility.combine(getFlags(), ',').toUpperCase(), mId + mDb.execSQL("UPDATE messages " + + "SET flags = ?, store_flag_1 = ?, store_flag_2 = ? " + + "WHERE id = ?", + new Object[] { + makeFlagsString(this), + makeFlagNumeric(this, Flag.X_STORE_1), + makeFlagNumeric(this, Flag.X_STORE_2), + mId }); } } + /** + * Convert *old* flags to flags string. Some flags are kept in their own columns + * (for selecting) and are not included here. + * @param message The message containing the flag(s) + * @return a comma-separated list of flags, to write into the "flags" column + */ + /* package */ String makeFlagsString(Message message) { + StringBuilder sb = null; + boolean nonEmpty = false; + for (Flag flag : Flag.values()) { + if ((flag != Flag.X_STORE_1 && flag != Flag.X_STORE_2) && message.isSet(flag)) { + if (sb == null) { + sb = new StringBuilder(); + } + if (nonEmpty) { + sb.append(','); + } + sb.append(flag.toString()); + nonEmpty = true; + } + } + return (sb == null) ? null : sb.toString(); + } + + /** + * Convert flags to numeric form (0 or 1) for database storage. + * @param message The message containing the flag of interest + * @param flag The flag of interest + * + */ + /* package */ int makeFlagNumeric(Message message, Flag flag) { + return message.isSet(flag) ? 1 : 0; + } + + public class LocalAttachmentBodyPart extends MimeBodyPart { private long mAttachmentId = -1; diff --git a/tests/src/com/android/email/mail/FlagTests.java b/tests/src/com/android/email/mail/FlagTests.java new file mode 100644 index 000000000..286978638 --- /dev/null +++ b/tests/src/com/android/email/mail/FlagTests.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.email.mail; + +import android.test.suitebuilder.annotation.SmallTest; + +import junit.framework.TestCase; + +/** + * Tests of Flag enum + */ +@SmallTest +public class FlagTests extends TestCase { + + /** + * Confirm that all flags are upper-case. This removes the need for wasteful toUpper + * conversions in code that uses the flags. + */ + public void testFlagsUpperCase() { + for (Flag flag : Flag.values()) { + String name = flag.name(); + assertEquals(name.toUpperCase(), name); + } + } +} diff --git a/tests/src/com/android/email/mail/internet/MimeMessageTest.java b/tests/src/com/android/email/mail/internet/MimeMessageTest.java index ecfe6df5e..5087139fe 100644 --- a/tests/src/com/android/email/mail/internet/MimeMessageTest.java +++ b/tests/src/com/android/email/mail/internet/MimeMessageTest.java @@ -225,23 +225,16 @@ public class MimeMessageTest extends TestCase { message.setFlag(Flag.X_STORE_1, true); assertTrue(message.isSet(Flag.X_STORE_1)); assertFalse(message.isSet(Flag.X_STORE_2)); - assertFalse(message.isSet(Flag.X_STORE_3)); - assertFalse(message.isSet(Flag.X_STORE_4)); // Set another message.setFlag(Flag.X_STORE_2, true); assertTrue(message.isSet(Flag.X_STORE_1)); assertTrue(message.isSet(Flag.X_STORE_2)); - assertFalse(message.isSet(Flag.X_STORE_3)); - assertFalse(message.isSet(Flag.X_STORE_4)); // Set some and clear some message.setFlag(Flag.X_STORE_1, false); - message.setFlag(Flag.X_STORE_3, true); assertFalse(message.isSet(Flag.X_STORE_1)); assertTrue(message.isSet(Flag.X_STORE_2)); - assertTrue(message.isSet(Flag.X_STORE_3)); - assertFalse(message.isSet(Flag.X_STORE_4)); } diff --git a/tests/src/com/android/email/mail/store/LocalStoreUnitTests.java b/tests/src/com/android/email/mail/store/LocalStoreUnitTests.java index 83fbafc3d..b9504585d 100644 --- a/tests/src/com/android/email/mail/store/LocalStoreUnitTests.java +++ b/tests/src/com/android/email/mail/store/LocalStoreUnitTests.java @@ -57,7 +57,7 @@ public class LocalStoreUnitTests extends AndroidTestCase { private static final String MESSAGE_ID = "Test-Message-ID"; private static final String MESSAGE_ID_2 = "Test-Message-ID-Second"; - private static final int DATABASE_VERSION = 21; + private static final int DATABASE_VERSION = 22; /* These values are provided by setUp() */ private String mLocalStoreUri = null; @@ -252,6 +252,72 @@ public class LocalStoreUnitTests extends AndroidTestCase { assertEquals("value-2-2b", callbacks2.getPersistentString("key2", null)); // changed } + /** + * Test functionality of persistence update with bulk update + */ + public void testPersistentBulkUpdate() throws MessagingException { + mFolder.open(OpenMode.READ_WRITE, null); + + // set up a 2nd folder to confirm independent storage + LocalStore.LocalFolder folder2 = (LocalStore.LocalFolder) mStore.getFolder("FOLDER-2"); + assertFalse(folder2.exists()); + folder2.create(FolderType.HOLDS_MESSAGES); + folder2.open(OpenMode.READ_WRITE, null); + + // use the callbacks, as these are the "official" API + Folder.PersistentDataCallbacks callbacks = mFolder.getPersistentCallbacks(); + Folder.PersistentDataCallbacks callbacks2 = folder2.getPersistentCallbacks(); + + // set some values - tests independence & inserts + callbacks.setPersistentString("key1", "value-1-1"); + callbacks.setPersistentString("key2", "value-1-2"); + callbacks2.setPersistentString("key1", "value-2-1"); + callbacks2.setPersistentString("key2", "value-2-2"); + + final MimeMessage message1 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY); + message1.setFlag(Flag.X_STORE_1, false); + message1.setFlag(Flag.X_STORE_2, false); + + final MimeMessage message2 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY); + message2.setFlag(Flag.X_STORE_1, true); + message2.setFlag(Flag.X_STORE_2, false); + + final MimeMessage message3 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY); + message3.setFlag(Flag.X_STORE_1, false); + message3.setFlag(Flag.X_STORE_2, true); + + final MimeMessage message4 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY); + message4.setFlag(Flag.X_STORE_1, true); + message4.setFlag(Flag.X_STORE_2, true); + + Message[] allOriginals = new Message[]{ message1, message2, message3, message4 }; + + mFolder.appendMessages(allOriginals); + + // Now make a bulk update (set) + callbacks.setPersistentStringAndMessageFlags("key1", "value-1-1a", + new Flag[]{ Flag.X_STORE_1 }, null); + // And check all messages for that flag now set, but other flag was not set + Message[] messages = mFolder.getMessages(null); + for (Message msg : messages) { + assertTrue(msg.isSet(Flag.X_STORE_1)); + if (msg.getUid().equals(message1.getUid())) assertFalse(msg.isSet(Flag.X_STORE_2)); + if (msg.getUid().equals(message2.getUid())) assertFalse(msg.isSet(Flag.X_STORE_2)); + } + assertEquals("value-1-1a", callbacks.getPersistentString("key1", null)); + + // Same test, but clearing + callbacks.setPersistentStringAndMessageFlags("key2", "value-1-2a", + null, new Flag[]{ Flag.X_STORE_2 }); + // And check all messages for that flag now set, but other flag was not set + messages = mFolder.getMessages(null); + for (Message msg : messages) { + assertTrue(msg.isSet(Flag.X_STORE_1)); + assertFalse(msg.isSet(Flag.X_STORE_2)); + } + assertEquals("value-1-2a", callbacks.getPersistentString("key2", null)); + } + /** * Test that messages are being stored with store flags properly persisted. * @@ -260,8 +326,8 @@ public class LocalStoreUnitTests extends AndroidTestCase { public void testStoreFlags() throws MessagingException { final MimeMessage message = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY); message.setMessageId(MESSAGE_ID); - message.setFlag(Flag.X_STORE_3, true); - message.setFlag(Flag.X_STORE_4, true); + message.setFlag(Flag.X_STORE_1, true); + message.setFlag(Flag.X_STORE_2, false); mFolder.open(OpenMode.READ_WRITE, null); mFolder.appendMessages(new Message[]{ message }); @@ -271,15 +337,13 @@ public class LocalStoreUnitTests extends AndroidTestCase { MimeMessage retrieved = (MimeMessage) mFolder.getMessage(localUid); assertEquals(MESSAGE_ID, retrieved.getMessageId()); - assertFalse(message.isSet(Flag.X_STORE_1)); + assertTrue(message.isSet(Flag.X_STORE_1)); assertFalse(message.isSet(Flag.X_STORE_2)); - assertTrue(message.isSet(Flag.X_STORE_3)); - assertTrue(message.isSet(Flag.X_STORE_4)); // Now try to update it using updateMessages() + retrieved.setFlag(Flag.X_STORE_1, false); retrieved.setFlag(Flag.X_STORE_2, true); - retrieved.setFlag(Flag.X_STORE_4, false); mFolder.updateMessage((LocalStore.LocalMessage)retrieved); // And read back once more to confirm the change (using getMessages() to confirm "just one") @@ -290,8 +354,136 @@ public class LocalStoreUnitTests extends AndroidTestCase { assertFalse(retrievedEntry.isSet(Flag.X_STORE_1)); assertTrue(retrievedEntry.isSet(Flag.X_STORE_2)); - assertTrue(retrievedEntry.isSet(Flag.X_STORE_3)); - assertFalse(retrievedEntry.isSet(Flag.X_STORE_4)); + } + + /** + * Test that store flags are separated into separate columns and not replicated in the + * (should be deprecated) string flags column. + */ + public void testStoreFlagStorage() throws MessagingException, URISyntaxException { + final MimeMessage message = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY); + message.setMessageId(MESSAGE_ID); + message.setFlag(Flag.SEEN, true); + message.setFlag(Flag.FLAGGED, true); + message.setFlag(Flag.X_STORE_1, true); + message.setFlag(Flag.X_STORE_2, true); + + mFolder.open(OpenMode.READ_WRITE, null); + mFolder.appendMessages(new Message[]{ message }); + String localUid = message.getUid(); + long folderId = mFolder.getId(); + mFolder.close(false); + + // read back using direct db calls, to view columns + final URI uri = new URI(mLocalStoreUri); + final String dbPath = uri.getPath(); + SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbPath, null); + + Cursor cursor = null; + try { + cursor = db.rawQuery( + "SELECT flags, store_flag_1, store_flag_2" + + " FROM messages" + + " WHERE uid = ? AND folder_id = ?", + new String[] { + localUid, Long.toString(folderId) + }); + if (!cursor.moveToNext()) { + fail("appended message not found"); + } + String flagString = cursor.getString(0); + String[] flags = flagString.split(","); + assertEquals(2, flags.length); // 2 = SEEN & FLAGGED + for (String flag : flags) { + assertFalse("storeFlag1 in string", flag.equals(Flag.X_STORE_1.toString())); + assertFalse("storeFlag2 in string", flag.equals(Flag.X_STORE_2.toString())); + } + + int flag1 = cursor.getInt(1); // store flag 1 is set + assertEquals(1, flag1); + int flag2 = cursor.getInt(2); // store flag 2 is set + assertEquals(1, flag2); + } + finally { + if (cursor != null) { + cursor.close(); + } + } + } + + /** + * Test the new functionality of getting messages from LocalStore based on their flags. + */ + public void testGetMessagesFlags() throws MessagingException { + + final MimeMessage message1 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY); + message1.setFlag(Flag.X_STORE_1, false); + message1.setFlag(Flag.X_STORE_2, false); + + final MimeMessage message2 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY); + message2.setFlag(Flag.X_STORE_1, true); + message2.setFlag(Flag.X_STORE_2, false); + + final MimeMessage message3 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY); + message3.setFlag(Flag.X_STORE_1, false); + message3.setFlag(Flag.X_STORE_2, true); + + final MimeMessage message4 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY); + message4.setFlag(Flag.X_STORE_1, true); + message4.setFlag(Flag.X_STORE_2, true); + + Message[] allOriginals = new Message[]{ message1, message2, message3, message4 }; + + mFolder.open(OpenMode.READ_WRITE, null); + mFolder.appendMessages(allOriginals); + mFolder.close(false); + + // Now try getting various permutation and see if it works + + // Null lists are the same as empty lists - return all messages + mFolder.open(OpenMode.READ_WRITE, null); + Message[] getAll1 = mFolder.getMessages(null, null, null); + checkGottenMessages("null filters", allOriginals, getAll1); + + Message[] getAll2 = mFolder.getMessages(new Flag[0], new Flag[0], null); + checkGottenMessages("empty filters", allOriginals, getAll2); + + // Now try some selections, trying set and clear cases + Message[] getSome1 = mFolder.getMessages(new Flag[]{ Flag.X_STORE_1 }, null, null); + checkGottenMessages("store_1 set", new Message[]{ message2, message4 }, getSome1); + + Message[] getSome2 = mFolder.getMessages(null, new Flag[]{ Flag.X_STORE_1 }, null); + checkGottenMessages("store_1 clear", new Message[]{ message1, message3 }, getSome2); + + Message[] getSome3 = mFolder.getMessages(new Flag[]{ Flag.X_STORE_2 }, null, null); + checkGottenMessages("store_2 set", new Message[]{ message3, message4 }, getSome3); + + Message[] getSome4 = mFolder.getMessages(null, new Flag[]{ Flag.X_STORE_2 }, null); + checkGottenMessages("store_2 clear", new Message[]{ message1, message2 }, getSome4); + + // Multi-flag selections + Message[] getSingle1 = mFolder.getMessages(new Flag[]{ Flag.X_STORE_1, Flag.X_STORE_2 }, + null, null); + checkGottenMessages("both set", new Message[]{ message4 }, getSingle1); + + Message[] getSingle2 = mFolder.getMessages(null, + new Flag[]{ Flag.X_STORE_1, Flag.X_STORE_2 }, null); + checkGottenMessages("both clear", new Message[]{ message1 }, getSingle2); + } + + /** + * Check for matching uid's between two lists of messages + */ + private void checkGottenMessages(String failMessage, Message[] expected, Message[] actual) { + HashSet expectedUids = new HashSet(); + for (Message message : expected) { + expectedUids.add(message.getUid()); + } + HashSet actualUids = new HashSet(); + for (Message message : actual) { + actualUids.add(message.getUid()); + } + assertEquals(failMessage, expectedUids, actualUids); } /** @@ -585,6 +777,29 @@ public class LocalStoreUnitTests extends AndroidTestCase { checkAllTablesFound(db); } + /** + * Check upgrade from db version 21 to latest + */ + public void testDbUpgrade21ToLatest() throws MessagingException, URISyntaxException { + final URI uri = new URI(mLocalStoreUri); + final String dbPath = uri.getPath(); + SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbPath, null); + + // create sample version 21 db tables + createSampleDb(db, 21); + db.close(); + + // upgrade database 21 to latest + LocalStore.newInstance(mLocalStoreUri, getContext(), null); + + // database should be upgraded + db = SQLiteDatabase.openOrCreateDatabase(dbPath, null); + assertEquals("database should be upgraded", DATABASE_VERSION, db.getVersion()); + + // check for all "latest version" tables + checkAllTablesFound(db); + } + /** * Checks the database to confirm that all tables, with all expected columns are found. */ @@ -602,7 +817,7 @@ public class LocalStoreUnitTests extends AndroidTestCase { new String[]{ "id", "folder_id", "uid", "subject", "date", "flags", "sender_list", "to_list", "cc_list", "bcc_list", "reply_to_list", "html_content", "text_content", "attachment_count", - "internal_date" } + "internal_date", "store_flag_1", "store_flag_2" } )); assertTrue("messages", foundNames.containsAll(expectedNames)); @@ -637,6 +852,7 @@ public class LocalStoreUnitTests extends AndroidTestCase { "html_content TEXT, text_content TEXT, attachment_count INTEGER, " + "internal_date INTEGER" + ((version >= 19) ? ", message_id TEXT" : "") + + ((version >= 22) ? ", store_flag_1 INTEGER, store_flag_2 INTEGER" : "") + ")"); db.execSQL("DROP TABLE IF EXISTS attachments"); db.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER," +