Home | History | Annotate | Download | only in store
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.email.mail.store;
     18 
     19 import com.android.email.Email;
     20 import com.android.email.mail.Address;
     21 import com.android.email.mail.Body;
     22 import com.android.email.mail.FetchProfile;
     23 import com.android.email.mail.Flag;
     24 import com.android.email.mail.Folder;
     25 import com.android.email.mail.Message;
     26 import com.android.email.mail.MessageTestUtils;
     27 import com.android.email.mail.MessagingException;
     28 import com.android.email.mail.Part;
     29 import com.android.email.mail.Store;
     30 import com.android.email.mail.Folder.FolderType;
     31 import com.android.email.mail.Folder.OpenMode;
     32 import com.android.email.mail.Message.RecipientType;
     33 import com.android.email.mail.MessageTestUtils.MultipartBuilder;
     34 import com.android.email.mail.internet.MimeMessage;
     35 import com.android.email.mail.internet.MimeUtility;
     36 import com.android.email.mail.internet.TextBody;
     37 import com.android.email.mail.store.LocalStore.LocalMessage;
     38 
     39 import android.content.ContentValues;
     40 import android.database.Cursor;
     41 import android.database.sqlite.SQLiteDatabase;
     42 import android.test.AndroidTestCase;
     43 import android.test.suitebuilder.annotation.MediumTest;
     44 
     45 import java.io.File;
     46 import java.net.URI;
     47 import java.net.URISyntaxException;
     48 import java.util.ArrayList;
     49 import java.util.Arrays;
     50 import java.util.HashSet;
     51 
     52 /**
     53  * This is a series of unit tests for the LocalStore class.
     54  */
     55 @MediumTest
     56 public class LocalStoreUnitTests extends AndroidTestCase {
     57 
     58     public static final String DB_NAME = "com.android.email.mail.store.LocalStoreUnitTests.db";
     59 
     60     private static final String SENDER = "sender (at) android.com";
     61     private static final String RECIPIENT_TO = "recipient-to (at) android.com";
     62     private static final String SUBJECT = "This is the subject";
     63     private static final String BODY = "This is the body.  This is also the body.";
     64     private static final String MESSAGE_ID = "Test-Message-ID";
     65     private static final String MESSAGE_ID_2 = "Test-Message-ID-Second";
     66 
     67     private static final int DATABASE_VERSION = 24;
     68 
     69     private static final String FOLDER_NAME = "TEST";
     70     private static final String MISSING_FOLDER_NAME = "TEST-NO-FOLDER";
     71 
     72     /* These values are provided by setUp() */
     73     private String mLocalStoreUri = null;
     74     private LocalStore mStore = null;
     75     private LocalStore.LocalFolder mFolder = null;
     76     private File mCacheDir;
     77 
     78     /**
     79      * Setup code.  We generate a lightweight LocalStore and LocalStore.LocalFolder.
     80      */
     81     @Override
     82     protected void setUp() throws Exception {
     83         super.setUp();
     84         Email.setTempDirectory(getContext());
     85 
     86         // These are needed so we can get at the inner classes
     87         // Create a dummy database (be sure to delete it in tearDown())
     88         mLocalStoreUri = "local://localhost/" + getContext().getDatabasePath(DB_NAME);
     89 
     90         mStore = (LocalStore) LocalStore.newInstance(mLocalStoreUri, getContext(), null);
     91         mFolder = (LocalStore.LocalFolder) mStore.getFolder(FOLDER_NAME);
     92     }
     93 
     94     /**
     95      * Teardown code.  Delete the local database and any other files
     96      */
     97     @Override
     98     protected void tearDown() throws Exception {
     99         super.tearDown();
    100         if (mFolder != null) {
    101             mFolder.close(false);
    102         }
    103 
    104         // First, try the official way
    105         if (mStore != null) {
    106             mStore.delete();
    107         }
    108 
    109         // Next, just try hacking and slashing files
    110         // (Mostly, this is actually copied from LocalStore.delete
    111         URI uri = new URI(mLocalStoreUri);
    112         String path = uri.getPath();
    113         File attachmentsDir = new File(path + "_att");
    114 
    115         // Delete any attachments we dribbled out
    116         try {
    117             File[] attachments = attachmentsDir.listFiles();
    118             for (File attachment : attachments) {
    119                 if (attachment.exists()) {
    120                     attachment.delete();
    121                 }
    122             }
    123         } catch (RuntimeException e) { }
    124         // Delete attachments dir
    125         try {
    126             if (attachmentsDir.exists()) {
    127                 attachmentsDir.delete();
    128             }
    129         } catch (RuntimeException e) { }
    130         // Delete db file
    131         try {
    132             new File(path).delete();
    133         }
    134         catch (RuntimeException e) { }
    135     }
    136 
    137     /**
    138      * Test that messages are being stored with Message-ID intact.
    139      *
    140      * This variant tests appendMessages() and getMessage() and getMessages()
    141      */
    142     public void testMessageId_1() throws MessagingException {
    143         final MimeMessage message = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    144         message.setMessageId(MESSAGE_ID);
    145         mFolder.open(OpenMode.READ_WRITE, null);
    146         mFolder.appendMessages(new Message[]{ message });
    147         String localUid = message.getUid();
    148 
    149         // Now try to read it back from the database using getMessage()
    150 
    151         MimeMessage retrieved = (MimeMessage) mFolder.getMessage(localUid);
    152         assertEquals(MESSAGE_ID, retrieved.getMessageId());
    153 
    154         // Now try to read it back from the database using getMessages()
    155 
    156         Message[] retrievedArray = mFolder.getMessages(null);
    157         assertEquals(1, retrievedArray.length);
    158         MimeMessage retrievedEntry = (MimeMessage) retrievedArray[0];
    159         assertEquals(MESSAGE_ID, retrievedEntry.getMessageId());
    160     }
    161 
    162     /**
    163      * Test that messages are being stored with Message-ID intact.
    164      *
    165      * This variant tests updateMessage() and getMessages()
    166      */
    167     public void testMessageId_2() throws MessagingException {
    168         final MimeMessage message = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    169         message.setMessageId(MESSAGE_ID);
    170         mFolder.open(OpenMode.READ_WRITE, null);
    171         mFolder.appendMessages(new Message[]{ message });
    172         String localUid = message.getUid();
    173 
    174         // Now try to read it back from the database using getMessage()
    175         MimeMessage retrieved = (MimeMessage) mFolder.getMessage(localUid);
    176         assertEquals(MESSAGE_ID, retrieved.getMessageId());
    177 
    178         // Now change the Message-ID and try to update() the message
    179         // Note, due to a weakness in the API, you have to use a message object you got from
    180         // LocalStore when making the update call
    181         retrieved.setMessageId(MESSAGE_ID_2);
    182         mFolder.updateMessage((LocalStore.LocalMessage)retrieved);
    183 
    184         // And read back once more to confirm the change (using getMessages() to confirm "just one")
    185         Message[] retrievedArray = mFolder.getMessages(null);
    186         assertEquals(1, retrievedArray.length);
    187         MimeMessage retrievedEntry = (MimeMessage) retrievedArray[0];
    188         assertEquals(MESSAGE_ID_2, retrievedEntry.getMessageId());
    189     }
    190 
    191     /**
    192      * Build a test message that can be used as input to processSourceMessage
    193      *
    194      * @param to Recipient(s) of the message
    195      * @param sender Sender(s) of the message
    196      * @param subject Subject of the message
    197      * @param content Content of the message
    198      * @return a complete Message object
    199      */
    200     private MimeMessage buildTestMessage(String to, String sender, String subject, String content)
    201             throws MessagingException {
    202         MimeMessage message = new MimeMessage();
    203 
    204         if (to != null) {
    205             Address[] addresses = Address.parse(to);
    206             message.setRecipients(RecipientType.TO, addresses);
    207         }
    208 
    209         if (sender != null) {
    210             Address[] addresses = Address.parse(sender);
    211             message.setFrom(Address.parse(sender)[0]);
    212         }
    213 
    214         if (subject != null) {
    215             message.setSubject(subject);
    216         }
    217 
    218         if (content != null) {
    219             TextBody body = new TextBody(content);
    220             message.setBody(body);
    221         }
    222 
    223         return message;
    224     }
    225 
    226     /**
    227      * Test two modes (STRUCTURE vs. BODY) of fetch()
    228      */
    229     public void testFetchModes() throws MessagingException {
    230         final String BODY_TEXT_PLAIN = "This is the body text.";
    231         final String BODY_TEXT_HTML = "But this is the HTML version of the body text.";
    232 
    233         MimeMessage message = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    234         message.setMessageId(MESSAGE_ID);
    235         Body body = new MultipartBuilder("multipart/mixed")
    236             .addBodyPart(MessageTestUtils.bodyPart("image/tiff", "cid.4 (at) android.com"))
    237             .addBodyPart(new MultipartBuilder("multipart/related")
    238                 .addBodyPart(new MultipartBuilder("multipart/alternative")
    239                     .addBodyPart(MessageTestUtils.textPart("text/plain", BODY_TEXT_PLAIN))
    240                     .addBodyPart(MessageTestUtils.textPart("text/html", BODY_TEXT_HTML))
    241                     .buildBodyPart())
    242                 .buildBodyPart())
    243             .addBodyPart(MessageTestUtils.bodyPart("image/gif", "cid.3 (at) android.com"))
    244             .build();
    245         message.setBody(body);
    246 
    247         mFolder.open(OpenMode.READ_WRITE, null);
    248         mFolder.appendMessages(new Message[]{ message });
    249 
    250         // Now read it back, and fetch it two ways - first, structure only
    251         Message[] messages = mFolder.getMessages(null);
    252         FetchProfile fp = new FetchProfile();
    253         fp.add(FetchProfile.Item.STRUCTURE);
    254         mFolder.fetch(messages, fp, null);
    255         // check for empty body parts
    256         Part textPart = MimeUtility.findFirstPartByMimeType(messages[0], "text/plain");
    257         Part htmlPart = MimeUtility.findFirstPartByMimeType(messages[0], "text/html");
    258         assertNull(MimeUtility.getTextFromPart(textPart));
    259         assertNull(MimeUtility.getTextFromPart(htmlPart));
    260 
    261         // Next, complete body
    262         messages = mFolder.getMessages(null);
    263         fp.clear();
    264         fp.add(FetchProfile.Item.BODY);
    265         mFolder.fetch(messages, fp, null);
    266         // check for real body parts
    267         textPart = MimeUtility.findFirstPartByMimeType(messages[0], "text/plain");
    268         htmlPart = MimeUtility.findFirstPartByMimeType(messages[0], "text/html");
    269         assertEquals(BODY_TEXT_PLAIN, MimeUtility.getTextFromPart(textPart));
    270         assertEquals(BODY_TEXT_HTML, MimeUtility.getTextFromPart(htmlPart));
    271     }
    272 
    273     /**
    274      * Test the new store persistent data code.
    275      *
    276      * This test, and the underlying code, reflect the essential error in the Account class.  The
    277      * account objects should have been singletons-per-account.  As it stands there are lots of
    278      * them floating around, which is very expensive (we waste a lot of effort creating them)
    279      * and forces slow sync hacks for dynamic data like the store's persistent data.
    280      */
    281     public void testStorePersistentData() {
    282 
    283         final String TEST_KEY = "the.test.key";
    284         final String TEST_KEY_2 = "a.different.test.key";
    285         final String TEST_STRING = "This is the store's persistent data.";
    286         final String TEST_STRING_2 = "Rewrite the store data.";
    287 
    288         // confirm default reads on new store
    289         assertEquals("the-default", mStore.getPersistentString(TEST_KEY, "the-default"));
    290 
    291         // test write/readback
    292         mStore.setPersistentString(TEST_KEY, TEST_STRING);
    293         mStore.setPersistentString(TEST_KEY_2, TEST_STRING_2);
    294         assertEquals(TEST_STRING, mStore.getPersistentString(TEST_KEY, null));
    295         assertEquals(TEST_STRING_2, mStore.getPersistentString(TEST_KEY_2, null));
    296     }
    297 
    298     /**
    299      * Test the callbacks for setting & getting persistent data
    300      */
    301     public void testStorePersistentCallbacks() throws MessagingException {
    302 
    303         final String TEST_KEY = "the.test.key";
    304         final String TEST_KEY_2 = "a.different.test.key";
    305         final String TEST_STRING = "This is the store's persistent data.";
    306         final String TEST_STRING_2 = "Rewrite the store data.";
    307 
    308         Store.PersistentDataCallbacks callbacks = mStore.getPersistentCallbacks();
    309 
    310         // confirm default reads on new store
    311         assertEquals("the-default", callbacks.getPersistentString(TEST_KEY, "the-default"));
    312 
    313         // test write/readback
    314         callbacks.setPersistentString(TEST_KEY, TEST_STRING);
    315         callbacks.setPersistentString(TEST_KEY_2, TEST_STRING_2);
    316         assertEquals(TEST_STRING, mStore.getPersistentString(TEST_KEY, null));
    317         assertEquals(TEST_STRING_2, mStore.getPersistentString(TEST_KEY_2, null));
    318     }
    319 
    320     /**
    321      * Test functionality of setting & saving store persistence values
    322      */
    323     public void testFolderPersistentStorage() throws MessagingException {
    324         mFolder.open(OpenMode.READ_WRITE, null);
    325 
    326         // set up a 2nd folder to confirm independent storage
    327         LocalStore.LocalFolder folder2 = (LocalStore.LocalFolder) mStore.getFolder("FOLDER-2");
    328         assertFalse(folder2.exists());
    329         folder2.create(FolderType.HOLDS_MESSAGES);
    330         folder2.open(OpenMode.READ_WRITE, null);
    331 
    332         // use the callbacks, as these are the "official" API
    333         Folder.PersistentDataCallbacks callbacks = mFolder.getPersistentCallbacks();
    334         Folder.PersistentDataCallbacks callbacks2 = folder2.getPersistentCallbacks();
    335 
    336         // set some values - tests independence & inserts
    337         callbacks.setPersistentString("key1", "value-1-1");
    338         callbacks.setPersistentString("key2", "value-1-2");
    339         callbacks2.setPersistentString("key1", "value-2-1");
    340         callbacks2.setPersistentString("key2", "value-2-2");
    341 
    342         // readback initial values
    343         assertEquals("value-1-1", callbacks.getPersistentString("key1", null));
    344         assertEquals("value-1-2", callbacks.getPersistentString("key2", null));
    345         assertEquals("value-2-1", callbacks2.getPersistentString("key1", null));
    346         assertEquals("value-2-2", callbacks2.getPersistentString("key2", null));
    347 
    348         // readback with default values
    349         assertEquals("value-1-3", callbacks.getPersistentString("key3", "value-1-3"));
    350         assertEquals("value-2-3", callbacks2.getPersistentString("key3", "value-2-3"));
    351 
    352         // partial updates
    353         callbacks.setPersistentString("key1", "value-1-1b");
    354         callbacks2.setPersistentString("key2", "value-2-2b");
    355         assertEquals("value-1-1b", callbacks.getPersistentString("key1", null));    // changed
    356         assertEquals("value-1-2", callbacks.getPersistentString("key2", null));     // same
    357         assertEquals("value-2-1", callbacks2.getPersistentString("key1", null));    // same
    358         assertEquals("value-2-2b", callbacks2.getPersistentString("key2", null));   // changed
    359     }
    360 
    361     /**
    362      * Test functionality of persistence update with bulk update
    363      */
    364     public void testFolderPersistentBulkUpdate() throws MessagingException {
    365         mFolder.open(OpenMode.READ_WRITE, null);
    366 
    367         // set up a 2nd folder to confirm independent storage
    368         LocalStore.LocalFolder folder2 = (LocalStore.LocalFolder) mStore.getFolder("FOLDER-2");
    369         assertFalse(folder2.exists());
    370         folder2.create(FolderType.HOLDS_MESSAGES);
    371         folder2.open(OpenMode.READ_WRITE, null);
    372 
    373         // use the callbacks, as these are the "official" API
    374         Folder.PersistentDataCallbacks callbacks = mFolder.getPersistentCallbacks();
    375         Folder.PersistentDataCallbacks callbacks2 = folder2.getPersistentCallbacks();
    376 
    377         // set some values - tests independence & inserts
    378         callbacks.setPersistentString("key1", "value-1-1");
    379         callbacks.setPersistentString("key2", "value-1-2");
    380         callbacks2.setPersistentString("key1", "value-2-1");
    381         callbacks2.setPersistentString("key2", "value-2-2");
    382 
    383         final MimeMessage message1 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    384         message1.setFlag(Flag.X_STORE_1, false);
    385         message1.setFlag(Flag.X_STORE_2, false);
    386 
    387         final MimeMessage message2 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    388         message2.setFlag(Flag.X_STORE_1, true);
    389         message2.setFlag(Flag.X_STORE_2, false);
    390 
    391         final MimeMessage message3 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    392         message3.setFlag(Flag.X_STORE_1, false);
    393         message3.setFlag(Flag.X_STORE_2, true);
    394 
    395         final MimeMessage message4 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    396         message4.setFlag(Flag.X_STORE_1, true);
    397         message4.setFlag(Flag.X_STORE_2, true);
    398 
    399         Message[] allOriginals = new Message[]{ message1, message2, message3, message4 };
    400 
    401         mFolder.appendMessages(allOriginals);
    402 
    403         // Now make a bulk update (set)
    404         callbacks.setPersistentStringAndMessageFlags("key1", "value-1-1a",
    405                 new Flag[]{ Flag.X_STORE_1 }, null);
    406         // And check all messages for that flag now set, but other flag was not set
    407         Message[] messages = mFolder.getMessages(null);
    408         for (Message msg : messages) {
    409             assertTrue(msg.isSet(Flag.X_STORE_1));
    410             if (msg.getUid().equals(message1.getUid())) assertFalse(msg.isSet(Flag.X_STORE_2));
    411             if (msg.getUid().equals(message2.getUid())) assertFalse(msg.isSet(Flag.X_STORE_2));
    412         }
    413         assertEquals("value-1-1a", callbacks.getPersistentString("key1", null));
    414 
    415         // Same test, but clearing
    416         callbacks.setPersistentStringAndMessageFlags("key2", "value-1-2a",
    417                 null, new Flag[]{ Flag.X_STORE_2 });
    418         // And check all messages for that flag now set, but other flag was not set
    419         messages = mFolder.getMessages(null);
    420         for (Message msg : messages) {
    421             assertTrue(msg.isSet(Flag.X_STORE_1));
    422             assertFalse(msg.isSet(Flag.X_STORE_2));
    423         }
    424         assertEquals("value-1-2a", callbacks.getPersistentString("key2", null));
    425     }
    426 
    427     /**
    428      * Test that messages are being stored with store flags properly persisted.
    429      *
    430      * This variant tests appendMessages() and updateMessages() and getMessage()
    431      */
    432     public void testStoreFlags() throws MessagingException {
    433         final MimeMessage message = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    434         message.setMessageId(MESSAGE_ID);
    435         message.setFlag(Flag.X_STORE_1, true);
    436         message.setFlag(Flag.X_STORE_2, false);
    437 
    438         mFolder.open(OpenMode.READ_WRITE, null);
    439         mFolder.appendMessages(new Message[]{ message });
    440         String localUid = message.getUid();
    441 
    442         // Now try to read it back from the database using getMessage()
    443 
    444         MimeMessage retrieved = (MimeMessage) mFolder.getMessage(localUid);
    445         assertEquals(MESSAGE_ID, retrieved.getMessageId());
    446         assertTrue(message.isSet(Flag.X_STORE_1));
    447         assertFalse(message.isSet(Flag.X_STORE_2));
    448 
    449         // Now try to update it using updateMessages()
    450 
    451         retrieved.setFlag(Flag.X_STORE_1, false);
    452         retrieved.setFlag(Flag.X_STORE_2, true);
    453         mFolder.updateMessage((LocalStore.LocalMessage)retrieved);
    454 
    455         // And read back once more to confirm the change (using getMessages() to confirm "just one")
    456         Message[] retrievedArray = mFolder.getMessages(null);
    457         assertEquals(1, retrievedArray.length);
    458         MimeMessage retrievedEntry = (MimeMessage) retrievedArray[0];
    459         assertEquals(MESSAGE_ID, retrieved.getMessageId());
    460 
    461         assertFalse(retrievedEntry.isSet(Flag.X_STORE_1));
    462         assertTrue(retrievedEntry.isSet(Flag.X_STORE_2));
    463     }
    464 
    465     /**
    466      * Test that messages are being stored with download & delete state flags properly persisted.
    467      *
    468      * This variant tests appendMessages() and updateMessages() and getMessage()
    469      */
    470     public void testDownloadAndDeletedFlags() throws MessagingException {
    471         final MimeMessage message = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    472         message.setMessageId(MESSAGE_ID);
    473         message.setFlag(Flag.X_STORE_1, true);
    474         message.setFlag(Flag.X_STORE_2, false);
    475         message.setFlag(Flag.X_DOWNLOADED_FULL, true);
    476         message.setFlag(Flag.X_DOWNLOADED_PARTIAL, false);
    477         message.setFlag(Flag.DELETED, false);
    478 
    479         mFolder.open(OpenMode.READ_WRITE, null);
    480         mFolder.appendMessages(new Message[]{ message });
    481         String localUid = message.getUid();
    482 
    483         // Now try to read it back from the database using getMessage()
    484 
    485         MimeMessage retrieved = (MimeMessage) mFolder.getMessage(localUid);
    486         assertEquals(MESSAGE_ID, retrieved.getMessageId());
    487         assertTrue(retrieved.isSet(Flag.X_STORE_1));
    488         assertFalse(retrieved.isSet(Flag.X_STORE_2));
    489         assertTrue(retrieved.isSet(Flag.X_DOWNLOADED_FULL));
    490         assertFalse(retrieved.isSet(Flag.X_DOWNLOADED_PARTIAL));
    491         assertFalse(retrieved.isSet(Flag.DELETED));
    492 
    493         // Now try to update it using updateMessages()
    494 
    495         retrieved.setFlag(Flag.X_STORE_1, false);
    496         retrieved.setFlag(Flag.X_STORE_2, true);
    497         retrieved.setFlag(Flag.X_DOWNLOADED_FULL, false);
    498         retrieved.setFlag(Flag.X_DOWNLOADED_PARTIAL, true);
    499         mFolder.updateMessage((LocalStore.LocalMessage)retrieved);
    500 
    501         // And read back once more to confirm the change (using getMessages() to confirm "just one")
    502         Message[] retrievedArray = mFolder.getMessages(null);
    503         assertEquals(1, retrievedArray.length);
    504         MimeMessage retrievedEntry = (MimeMessage) retrievedArray[0];
    505         assertEquals(MESSAGE_ID, retrievedEntry.getMessageId());
    506 
    507         assertFalse(retrievedEntry.isSet(Flag.X_STORE_1));
    508         assertTrue(retrievedEntry.isSet(Flag.X_STORE_2));
    509         assertFalse(retrievedEntry.isSet(Flag.X_DOWNLOADED_FULL));
    510         assertTrue(retrievedEntry.isSet(Flag.X_DOWNLOADED_PARTIAL));
    511         assertFalse(retrievedEntry.isSet(Flag.DELETED));
    512 
    513         // Finally test setFlag(Flag.DELETED)
    514         retrievedEntry.setFlag(Flag.DELETED, true);
    515         mFolder.updateMessage((LocalStore.LocalMessage)retrievedEntry);
    516         Message[] retrievedArray2 = mFolder.getMessages(null);
    517         assertEquals(1, retrievedArray2.length);
    518         MimeMessage retrievedEntry2 = (MimeMessage) retrievedArray2[0];
    519         assertEquals(MESSAGE_ID, retrievedEntry2.getMessageId());
    520 
    521         assertFalse(retrievedEntry2.isSet(Flag.X_STORE_1));
    522         assertTrue(retrievedEntry2.isSet(Flag.X_STORE_2));
    523         assertFalse(retrievedEntry2.isSet(Flag.X_DOWNLOADED_FULL));
    524         assertTrue(retrievedEntry2.isSet(Flag.X_DOWNLOADED_PARTIAL));
    525         assertTrue(retrievedEntry2.isSet(Flag.DELETED));
    526     }
    527 
    528     /**
    529      * Test that store flags are separated into separate columns and not replicated in the
    530      * (should be deprecated) string flags column.
    531      */
    532     public void testStoreFlagStorage() throws MessagingException, URISyntaxException {
    533         final MimeMessage message = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    534         message.setMessageId(MESSAGE_ID);
    535         message.setFlag(Flag.SEEN, true);
    536         message.setFlag(Flag.FLAGGED, true);
    537         message.setFlag(Flag.X_STORE_1, true);
    538         message.setFlag(Flag.X_STORE_2, true);
    539         message.setFlag(Flag.X_DOWNLOADED_FULL, true);
    540         message.setFlag(Flag.X_DOWNLOADED_PARTIAL, true);
    541         message.setFlag(Flag.DELETED, true);
    542 
    543         mFolder.open(OpenMode.READ_WRITE, null);
    544         mFolder.appendMessages(new Message[]{ message });
    545         String localUid = message.getUid();
    546         long folderId = mFolder.getId();
    547         mFolder.close(false);
    548 
    549         // read back using direct db calls, to view columns
    550         final URI uri = new URI(mLocalStoreUri);
    551         final String dbPath = uri.getPath();
    552         SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
    553 
    554         Cursor cursor = null;
    555         try {
    556             cursor = db.rawQuery(
    557                     "SELECT flags, store_flag_1, store_flag_2," +
    558                     " flag_downloaded_full, flag_downloaded_partial, flag_deleted" +
    559                     " FROM messages" +
    560                     " WHERE uid = ? AND folder_id = ?",
    561                     new String[] {
    562                             localUid, Long.toString(folderId)
    563                     });
    564             assertTrue("appended message not found", cursor.moveToNext());
    565             String flagString = cursor.getString(0);
    566             String[] flags = flagString.split(",");
    567             assertEquals(2, flags.length);      // 2 = SEEN & FLAGGED
    568             for (String flag : flags) {
    569                 assertFalse("storeFlag1 in string", flag.equals(Flag.X_STORE_1.toString()));
    570                 assertFalse("storeFlag2 in string", flag.equals(Flag.X_STORE_2.toString()));
    571                 assertFalse("flag_downloaded_full in string",
    572                         flag.equals(Flag.X_DOWNLOADED_FULL.toString()));
    573                 assertFalse("flag_downloaded_partial in string",
    574                         flag.equals(Flag.X_DOWNLOADED_PARTIAL.toString()));
    575                 assertFalse("flag_deleted in string", flag.equals(Flag.DELETED.toString()));
    576             }
    577             assertEquals(1, cursor.getInt(1));  // store flag 1 is set
    578             assertEquals(1, cursor.getInt(2));  // store flag 2 is set
    579             assertEquals(1, cursor.getInt(3));  // flag_downloaded_full is set
    580             assertEquals(1, cursor.getInt(4));  // flag_downloaded_partial is set
    581             assertEquals(1, cursor.getInt(5));  // flag_deleted is set
    582         }
    583         finally {
    584             if (cursor != null) {
    585                 cursor.close();
    586             }
    587         }
    588     }
    589 
    590     /**
    591      * Test the new functionality of getting messages from LocalStore based on their flags.
    592      */
    593     public void testGetMessagesFlags() throws MessagingException {
    594 
    595         final MimeMessage message1 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    596         message1.setFlag(Flag.X_STORE_1, false);
    597         message1.setFlag(Flag.X_STORE_2, false);
    598 
    599         final MimeMessage message2 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    600         message2.setFlag(Flag.X_STORE_1, true);
    601         message2.setFlag(Flag.X_STORE_2, false);
    602 
    603         final MimeMessage message3 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    604         message3.setFlag(Flag.X_STORE_1, false);
    605         message3.setFlag(Flag.X_STORE_2, true);
    606 
    607         final MimeMessage message4 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    608         message4.setFlag(Flag.X_STORE_1, true);
    609         message4.setFlag(Flag.X_STORE_2, true);
    610 
    611         final MimeMessage message5 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    612         message5.setFlag(Flag.X_DOWNLOADED_FULL, true);
    613 
    614         final MimeMessage message6 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    615         message6.setFlag(Flag.X_DOWNLOADED_PARTIAL, true);
    616 
    617         final MimeMessage message7 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    618         message7.setFlag(Flag.DELETED, true);
    619 
    620         Message[] allOriginals = new Message[] {
    621                 message1, message2, message3, message4, message5, message6, message7 };
    622 
    623         mFolder.open(OpenMode.READ_WRITE, null);
    624         mFolder.appendMessages(allOriginals);
    625         mFolder.close(false);
    626 
    627         // Now try getting various permutation and see if it works
    628 
    629         // Null lists are the same as empty lists - return all messages
    630         mFolder.open(OpenMode.READ_WRITE, null);
    631         Message[] getAll1 = mFolder.getMessages(null, null, null);
    632         checkGottenMessages("null filters", allOriginals, getAll1);
    633 
    634         Message[] getAll2 = mFolder.getMessages(new Flag[0], new Flag[0], null);
    635         checkGottenMessages("empty filters", allOriginals, getAll2);
    636 
    637         // Now try some selections, trying set and clear cases
    638         Message[] getSome1 = mFolder.getMessages(new Flag[]{ Flag.X_STORE_1 }, null, null);
    639         checkGottenMessages("store_1 set", new Message[]{ message2, message4 }, getSome1);
    640 
    641         Message[] getSome2 = mFolder.getMessages(null, new Flag[]{ Flag.X_STORE_1 }, null);
    642         checkGottenMessages("store_1 clear",
    643                 new Message[]{ message1, message3, message5, message6, message7 }, getSome2);
    644 
    645         Message[] getSome3 = mFolder.getMessages(new Flag[]{ Flag.X_STORE_2 }, null, null);
    646         checkGottenMessages("store_2 set", new Message[]{ message3, message4 }, getSome3);
    647 
    648         Message[] getSome4 = mFolder.getMessages(null, new Flag[]{ Flag.X_STORE_2 }, null);
    649         checkGottenMessages("store_2 clear",
    650                 new Message[]{ message1, message2, message5, message6, message7 }, getSome4);
    651 
    652         Message[] getOne1 = mFolder.getMessages(new Flag[]{ Flag.X_DOWNLOADED_FULL }, null, null);
    653         checkGottenMessages("downloaded full", new Message[]{ message5 }, getOne1);
    654 
    655         Message[] getOne2 = mFolder.getMessages(new Flag[]{ Flag.X_DOWNLOADED_PARTIAL }, null,
    656                 null);
    657         checkGottenMessages("downloaded partial", new Message[]{ message6 }, getOne2);
    658 
    659         Message[] getOne3 = mFolder.getMessages(new Flag[]{ Flag.DELETED }, null, null);
    660         checkGottenMessages("deleted", new Message[]{ message7 }, getOne3);
    661 
    662         // Multi-flag selections
    663         Message[] getSingle1 = mFolder.getMessages(new Flag[]{ Flag.X_STORE_1, Flag.X_STORE_2 },
    664                 null, null);
    665         checkGottenMessages("both set", new Message[]{ message4 }, getSingle1);
    666 
    667         Message[] getSingle2 = mFolder.getMessages(null,
    668                 new Flag[]{ Flag.X_STORE_1, Flag.X_STORE_2 }, null);
    669         checkGottenMessages("both clear", new Message[]{ message1, message5, message6, message7 },
    670                 getSingle2);
    671     }
    672 
    673     /**
    674      * Check for matching uid's between two lists of messages
    675      */
    676     private void checkGottenMessages(String failMessage, Message[] expected, Message[] actual) {
    677         HashSet<String> expectedUids = new HashSet<String>();
    678         for (Message message : expected) {
    679             expectedUids.add(message.getUid());
    680         }
    681         HashSet<String> actualUids = new HashSet<String>();
    682         for (Message message : actual) {
    683             actualUids.add(message.getUid());
    684         }
    685         assertEquals(failMessage, expectedUids, actualUids);
    686     }
    687 
    688     /**
    689      * Test for getMessageCount
    690      */
    691     public void testMessageCount() throws MessagingException {
    692 
    693         final MimeMessage message1 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    694         message1.setFlag(Flag.X_STORE_1, false);
    695         message1.setFlag(Flag.X_STORE_2, false);
    696         message1.setFlag(Flag.X_DOWNLOADED_FULL, true);
    697 
    698         final MimeMessage message2 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    699         message2.setFlag(Flag.X_STORE_1, true);
    700         message2.setFlag(Flag.X_STORE_2, false);
    701 
    702         final MimeMessage message3 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    703         message3.setFlag(Flag.X_STORE_1, false);
    704         message3.setFlag(Flag.X_STORE_2, true);
    705         message3.setFlag(Flag.X_DOWNLOADED_FULL, true);
    706 
    707         final MimeMessage message4 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    708         message4.setFlag(Flag.X_STORE_1, true);
    709         message4.setFlag(Flag.X_STORE_2, true);
    710         message4.setFlag(Flag.X_DOWNLOADED_FULL, true);
    711 
    712         final MimeMessage message5 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    713         message5.setFlag(Flag.X_DOWNLOADED_FULL, true);
    714 
    715         final MimeMessage message6 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    716         message6.setFlag(Flag.X_DOWNLOADED_PARTIAL, true);
    717 
    718         final MimeMessage message7 = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
    719         message7.setFlag(Flag.DELETED, true);
    720 
    721         Message[] allOriginals = new Message[] {
    722                 message1, message2, message3, message4, message5, message6, message7 };
    723 
    724         mFolder.open(OpenMode.READ_WRITE, null);
    725         mFolder.appendMessages(allOriginals);
    726         mFolder.close(false);
    727 
    728         // Null lists are the same as empty lists - return all messages
    729         mFolder.open(OpenMode.READ_WRITE, null);
    730 
    731         int allMessages = mFolder.getMessageCount();
    732         assertEquals("all messages", 7, allMessages);
    733 
    734         int storeFlag1 = mFolder.getMessageCount(new Flag[] { Flag.X_STORE_1 }, null);
    735         assertEquals("store flag 1", 2, storeFlag1);
    736 
    737         int storeFlag1NotFlag2 = mFolder.getMessageCount(
    738                 new Flag[] { Flag.X_STORE_1 }, new Flag[] { Flag.X_STORE_2 });
    739         assertEquals("store flag 1, not 2", 1, storeFlag1NotFlag2);
    740 
    741         int downloadedFull = mFolder.getMessageCount(new Flag[] { Flag.X_DOWNLOADED_FULL }, null);
    742         assertEquals("downloaded full", 4, downloadedFull);
    743 
    744         int storeFlag2Full = mFolder.getMessageCount(
    745                 new Flag[] { Flag.X_STORE_2, Flag.X_DOWNLOADED_FULL }, null);
    746         assertEquals("store flag 2, full", 2, storeFlag2Full);
    747 
    748         int notDeleted = mFolder.getMessageCount(null, new Flag[] { Flag.DELETED });
    749         assertEquals("not deleted", 6, notDeleted);
    750     }
    751 
    752     /**
    753      * Test unread messages count
    754      */
    755     public void testUnreadMessages() throws MessagingException {
    756         mFolder.open(OpenMode.READ_WRITE, null);
    757 
    758         // set up a 2nd folder to confirm independent storage
    759         LocalStore.LocalFolder folder2 = (LocalStore.LocalFolder) mStore.getFolder("FOLDER-2");
    760         assertFalse(folder2.exists());
    761         folder2.create(FolderType.HOLDS_MESSAGES);
    762         folder2.open(OpenMode.READ_WRITE, null);
    763 
    764         // read and write, look for independent storage
    765         mFolder.setUnreadMessageCount(400);
    766         folder2.setUnreadMessageCount(425);
    767 
    768         mFolder.close(false);
    769         folder2.close(false);
    770         mFolder.open(OpenMode.READ_WRITE, null);
    771         folder2.open(OpenMode.READ_WRITE, null);
    772 
    773         assertEquals(400, mFolder.getUnreadMessageCount());
    774         assertEquals(425, folder2.getUnreadMessageCount());
    775     }
    776 
    777     /**
    778      * Test unread messages count - concurrent access via two folder objects
    779      */
    780     public void testUnreadMessagesConcurrent() throws MessagingException {
    781         mFolder.open(OpenMode.READ_WRITE, null);
    782 
    783         // set up a 2nd folder to confirm concurrent access
    784         LocalStore.LocalFolder folder2 = (LocalStore.LocalFolder) mStore.getFolder(FOLDER_NAME);
    785         assertTrue(folder2.exists());
    786         folder2.open(OpenMode.READ_WRITE, null);
    787 
    788         // read and write, look for concurrent storage
    789         mFolder.setUnreadMessageCount(450);
    790         assertEquals(450, folder2.getUnreadMessageCount());
    791     }
    792 
    793     /**
    794      * Test visible limits support
    795      */
    796     public void testReadWriteVisibleLimits() throws MessagingException {
    797         mFolder.open(OpenMode.READ_WRITE, null);
    798 
    799         // set up a 2nd folder to confirm independent storage
    800         LocalStore.LocalFolder folder2 = (LocalStore.LocalFolder) mStore.getFolder("FOLDER-2");
    801         assertFalse(folder2.exists());
    802         folder2.create(FolderType.HOLDS_MESSAGES);
    803         folder2.open(OpenMode.READ_WRITE, null);
    804 
    805         // read and write, look for independent storage
    806         mFolder.setVisibleLimit(100);
    807         folder2.setVisibleLimit(200);
    808 
    809         mFolder.close(false);
    810         folder2.close(false);
    811         mFolder.open(OpenMode.READ_WRITE, null);
    812         folder2.open(OpenMode.READ_WRITE, null);
    813 
    814         assertEquals(100, mFolder.getVisibleLimit());
    815         assertEquals(200, folder2.getVisibleLimit());
    816     }
    817 
    818     /**
    819      * Test visible limits support - concurrent access via two folder objects
    820      */
    821     public void testVisibleLimitsConcurrent() throws MessagingException {
    822         mFolder.open(OpenMode.READ_WRITE, null);
    823 
    824         // set up a 2nd folder to confirm concurrent access
    825         LocalStore.LocalFolder folder2 = (LocalStore.LocalFolder) mStore.getFolder(FOLDER_NAME);
    826         assertTrue(folder2.exists());
    827         folder2.open(OpenMode.READ_WRITE, null);
    828 
    829         // read and write, look for concurrent storage
    830         mFolder.setVisibleLimit(300);
    831         assertEquals(300, folder2.getVisibleLimit());
    832     }
    833 
    834     /**
    835      * Test reset limits support
    836      */
    837     public void testResetVisibleLimits() throws MessagingException {
    838         mFolder.open(OpenMode.READ_WRITE, null);
    839 
    840         // set up a 2nd folder to confirm independent storage
    841         LocalStore.LocalFolder folder2 = (LocalStore.LocalFolder) mStore.getFolder("FOLDER-2");
    842         assertFalse(folder2.exists());
    843         folder2.create(FolderType.HOLDS_MESSAGES);
    844         folder2.open(OpenMode.READ_WRITE, null);
    845 
    846         // read and write, look for independent storage
    847         mFolder.setVisibleLimit(100);
    848         folder2.setVisibleLimit(200);
    849 
    850         mFolder.close(false);
    851         folder2.close(false);
    852         mFolder.open(OpenMode.READ_WRITE, null);
    853         folder2.open(OpenMode.READ_WRITE, null);
    854 
    855         mStore.resetVisibleLimits(Email.VISIBLE_LIMIT_DEFAULT);
    856         assertEquals(Email.VISIBLE_LIMIT_DEFAULT, mFolder.getVisibleLimit());
    857         assertEquals(Email.VISIBLE_LIMIT_DEFAULT, folder2.getVisibleLimit());
    858 
    859         mFolder.close(false);
    860         folder2.close(false);
    861     }
    862 
    863     /**
    864      * Lightweight test to confirm that LocalStore hasn't implemented any folder roles yet.
    865      */
    866     public void testNoFolderRolesYet() throws MessagingException {
    867         Folder[] localFolders = mStore.getPersonalNamespaces();
    868         for (Folder folder : localFolders) {
    869             assertEquals(Folder.FolderRole.UNKNOWN, folder.getRole());
    870         }
    871     }
    872 
    873     /**
    874      * Test missing folder (on open).  This should succeed because open will create it.
    875      */
    876     public void testMissingFolderOpen() throws MessagingException {
    877         Folder noFolder = mStore.getFolder(MISSING_FOLDER_NAME);
    878         noFolder.open(OpenMode.READ_WRITE, null);
    879         noFolder.close(false);
    880     }
    881 
    882     /**
    883      * Test missing folder (on getMessageCount).  This should not fail - it should return zero,
    884      * which is the actual count of messages in that folder.
    885      */
    886     public void testMissingFolderGetMessageCount() throws MessagingException {
    887         Folder noFolder = mStore.getFolder(MISSING_FOLDER_NAME);
    888         noFolder.open(OpenMode.READ_WRITE, null);
    889 
    890         // Now delete it behind its back
    891         Folder noFolder2 = mStore.getFolder(MISSING_FOLDER_NAME);
    892         noFolder2.delete(true);
    893 
    894         // Now try the call on the first instance
    895         int count = noFolder.getMessageCount();
    896         assertEquals(0, count);
    897     }
    898 
    899     /**
    900      * Test missing folder (on getUnreadMessageCount).  This should fail because we delete the
    901      * open folder, simulating multi-threading behavior.
    902      */
    903     public void testMissingFolderGetUnreadMessageCount() throws MessagingException {
    904         Folder noFolder = mStore.getFolder(MISSING_FOLDER_NAME);
    905         noFolder.open(OpenMode.READ_WRITE, null);
    906 
    907         // Now delete it behind its back
    908         Folder noFolder2 = mStore.getFolder(MISSING_FOLDER_NAME);
    909         noFolder2.delete(true);
    910 
    911         // Now try the call on the first instance
    912         try {
    913             noFolder.getUnreadMessageCount();
    914             fail("MessagingException expected");
    915         } catch (MessagingException me) {
    916             // OK - success.
    917         }
    918     }
    919 
    920     /**
    921      * Test missing folder (on getVisibleLimit).  This should fail because we delete the
    922      * open folder, simulating multi-threading behavior.
    923      */
    924     public void testMissingFolderGetVisibleLimit() throws MessagingException {
    925         LocalStore.LocalFolder noFolder =
    926                 (LocalStore.LocalFolder) mStore.getFolder(MISSING_FOLDER_NAME);
    927         noFolder.open(OpenMode.READ_WRITE, null);
    928 
    929         // Now delete it behind its back
    930         Folder noFolder2 = mStore.getFolder(MISSING_FOLDER_NAME);
    931         noFolder2.delete(true);
    932 
    933         // Now try the call on the first instance
    934         try {
    935             noFolder.getVisibleLimit();
    936             fail("MessagingException expected");
    937         } catch (MessagingException me) {
    938             // OK - success.
    939         }
    940     }
    941 
    942     /**
    943      * Test for setExtendedHeader() and getExtendedHeader()
    944      */
    945     public void testExtendedHeader() throws MessagingException {
    946         MimeMessage message = new MimeMessage();
    947         message.setUid("message1");
    948         mFolder.appendMessages(new Message[] { message });
    949 
    950         message.setUid("message2");
    951         message.setExtendedHeader("X-Header1", "value1");
    952         message.setExtendedHeader("X-Header2", "value2\r\n value3\n value4\r\n");
    953         mFolder.appendMessages(new Message[] { message });
    954 
    955         LocalMessage message1 = (LocalMessage) mFolder.getMessage("message1");
    956         assertNull("none existent header", message1.getExtendedHeader("X-None-Existent"));
    957 
    958         LocalMessage message2 = (LocalMessage) mFolder.getMessage("message2");
    959         assertEquals("header 1", "value1", message2.getExtendedHeader("X-Header1"));
    960         assertEquals("header 2", "value2 value3 value4", message2.getExtendedHeader("X-Header2"));
    961         assertNull("header 3", message2.getExtendedHeader("X-Header3"));
    962     }
    963 
    964     /**
    965      * Tests for database version.
    966      */
    967     public void testDbVersion() throws MessagingException, URISyntaxException {
    968         // build current version database.
    969         LocalStore.newInstance(mLocalStoreUri, getContext(), null);
    970         final URI uri = new URI(mLocalStoreUri);
    971         final String dbPath = uri.getPath();
    972         final SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
    973 
    974         // database version should be latest.
    975         assertEquals("database version should be latest", DATABASE_VERSION, db.getVersion());
    976         db.close();
    977     }
    978 
    979     /**
    980      * Helper function convert Cursor data to ContentValues
    981      */
    982     private ContentValues cursorToContentValues(Cursor c, String[] schema) {
    983         if (c.getColumnCount() != schema.length) {
    984             throw new IndexOutOfBoundsException("schema length is not mach with cursor columns");
    985         }
    986 
    987         final ContentValues cv = new ContentValues();
    988         for (int i = 0, count = c.getColumnCount(); i < count; ++i) {
    989             final String key = c.getColumnName(i);
    990             final String type = schema[i];
    991             if (type == "text") {
    992                 cv.put(key, c.getString(i));
    993             } else if (type == "integer" || type == "primary") {
    994                 cv.put(key, c.getLong(i));
    995             } else if (type == "numeric" || type == "real") {
    996                 cv.put(key, c.getDouble(i));
    997             } else if (type == "blob") {
    998                 cv.put(key, c.getBlob(i));
    999             } else {
   1000                 throw new IllegalArgumentException("unsupported type at index " + i);
   1001             }
   1002         }
   1003         return cv;
   1004     }
   1005 
   1006     /**
   1007      * Helper function to read out Cursor columns
   1008      */
   1009     private HashSet<String> cursorToColumnNames(Cursor c) {
   1010         HashSet<String> result = new HashSet<String>();
   1011         for (int i = 0, count = c.getColumnCount(); i < count; ++i) {
   1012             result.add(c.getColumnName(i));
   1013         }
   1014         return result;
   1015     }
   1016 
   1017     /**
   1018      * Tests for database upgrade from version 18 to current version.
   1019      */
   1020     public void testDbUpgrade18ToLatest() throws MessagingException, URISyntaxException {
   1021         final URI uri = new URI(mLocalStoreUri);
   1022         final String dbPath = uri.getPath();
   1023         SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
   1024 
   1025         // create sample version 18 db tables
   1026         createSampleDb(db, 18);
   1027 
   1028         // sample message data and expected data
   1029         final ContentValues initialMessage = new ContentValues();
   1030         initialMessage.put("folder_id", (long) 2);        // folder_id type integer == Long
   1031         initialMessage.put("internal_date", (long) 3);    // internal_date type integer == Long
   1032         final ContentValues expectedMessage = new ContentValues(initialMessage);
   1033         expectedMessage.put("id", db.insert("messages", null, initialMessage));
   1034 
   1035         // sample attachment data and expected data
   1036         final ContentValues initialAttachment = new ContentValues();
   1037         initialAttachment.put("message_id", (long) 4);    // message_id type integer == Long
   1038         initialAttachment.put("mime_type", (String) "a"); // mime_type type text == String
   1039         final ContentValues expectedAttachment = new ContentValues(initialAttachment);
   1040         expectedAttachment.put("id", db.insert("attachments", null, initialAttachment));
   1041         db.close();
   1042 
   1043         // upgrade database 18 to latest
   1044         LocalStore.newInstance(mLocalStoreUri, getContext(), null);
   1045 
   1046         // added message_id column should be initialized as null
   1047         expectedMessage.put("message_id", (String) null);    // message_id type text == String
   1048         // added content_id column should be initialized as null
   1049         expectedAttachment.put("content_id", (String) null); // content_id type text == String
   1050 
   1051         // database should be upgraded
   1052         db = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
   1053         assertEquals("database should be upgraded", DATABASE_VERSION, db.getVersion());
   1054         Cursor c;
   1055 
   1056         // check for all "latest version" tables
   1057         checkAllTablesFound(db);
   1058 
   1059         // check message table
   1060         c = db.query("messages",
   1061                 new String[] { "id", "folder_id", "internal_date", "message_id" },
   1062                 null, null, null, null, null);
   1063         // check if data is available
   1064         assertTrue("messages table should have one data", c.moveToNext());
   1065 
   1066         // check if data are expected
   1067         final ContentValues actualMessage = cursorToContentValues(c,
   1068                 new String[] { "primary", "integer", "integer", "text" });
   1069         assertEquals("messages table cursor does not have expected values",
   1070                 expectedMessage, actualMessage);
   1071         c.close();
   1072 
   1073         // check attachment table
   1074         c = db.query("attachments",
   1075                 new String[] { "id", "message_id", "mime_type", "content_id" },
   1076                 null, null, null, null, null);
   1077         // check if data is available
   1078         assertTrue("attachments table should have one data", c.moveToNext());
   1079 
   1080         // check if data are expected
   1081         final ContentValues actualAttachment = cursorToContentValues(c,
   1082                 new String[] { "primary", "integer", "text", "text" });
   1083         assertEquals("attachment table cursor does not have expected values",
   1084                 expectedAttachment, actualAttachment);
   1085         c.close();
   1086 
   1087         db.close();
   1088     }
   1089 
   1090     /**
   1091      * Tests for database upgrade from version 19 to current version.
   1092      */
   1093     public void testDbUpgrade19ToLatest() throws MessagingException, URISyntaxException {
   1094         final URI uri = new URI(mLocalStoreUri);
   1095         final String dbPath = uri.getPath();
   1096         SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
   1097 
   1098         // create sample version 19 db tables
   1099         createSampleDb(db, 19);
   1100 
   1101         // sample message data and expected data
   1102         final ContentValues initialMessage = new ContentValues();
   1103         initialMessage.put("folder_id", (long) 2);      // folder_id type integer == Long
   1104         initialMessage.put("internal_date", (long) 3);  // internal_date integer == Long
   1105         initialMessage.put("message_id", (String) "x"); // message_id text == String
   1106         final ContentValues expectedMessage = new ContentValues(initialMessage);
   1107         expectedMessage.put("id", db.insert("messages", null, initialMessage));
   1108 
   1109         // sample attachment data and expected data
   1110         final ContentValues initialAttachment = new ContentValues();
   1111         initialAttachment.put("message_id", (long) 4);  // message_id type integer == Long
   1112         initialAttachment.put("mime_type", (String) "a"); // mime_type type text == String
   1113         final ContentValues expectedAttachment = new ContentValues(initialAttachment);
   1114         expectedAttachment.put("id", db.insert("attachments", null, initialAttachment));
   1115 
   1116         db.close();
   1117 
   1118         // upgrade database 19 to latest
   1119         LocalStore.newInstance(mLocalStoreUri, getContext(), null);
   1120 
   1121         // added content_id column should be initialized as null
   1122         expectedAttachment.put("content_id", (String) null);  // content_id type text == String
   1123 
   1124         // database should be upgraded
   1125         db = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
   1126         assertEquals("database should be upgraded", DATABASE_VERSION, db.getVersion());
   1127         Cursor c;
   1128 
   1129         // check for all "latest version" tables
   1130         checkAllTablesFound(db);
   1131 
   1132         // check message table
   1133         c = db.query("messages",
   1134                 new String[] { "id", "folder_id", "internal_date", "message_id" },
   1135                 null, null, null, null, null);
   1136         // check if data is available
   1137         assertTrue("attachments table should have one data", c.moveToNext());
   1138 
   1139         // check if data are expected
   1140         final ContentValues actualMessage = cursorToContentValues(c,
   1141                 new String[] { "primary", "integer", "integer", "text" });
   1142         assertEquals("messages table cursor does not have expected values",
   1143                 expectedMessage, actualMessage);
   1144 
   1145         // check attachment table
   1146         c = db.query("attachments",
   1147                 new String[] { "id", "message_id", "mime_type", "content_id" },
   1148                 null, null, null, null, null);
   1149         // check if data is available
   1150         assertTrue("attachments table should have one data", c.moveToNext());
   1151 
   1152         // check if data are expected
   1153         final ContentValues actualAttachment = cursorToContentValues(c,
   1154                         new String[] { "primary", "integer", "text", "text" });
   1155         assertEquals("attachment table cursor does not have expected values",
   1156                 expectedAttachment, actualAttachment);
   1157 
   1158         db.close();
   1159     }
   1160 
   1161     /**
   1162      * Check upgrade from db version 20 to latest
   1163      */
   1164     public void testDbUpgrade20ToLatest() throws MessagingException, URISyntaxException {
   1165         final URI uri = new URI(mLocalStoreUri);
   1166         final String dbPath = uri.getPath();
   1167         SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
   1168 
   1169         // create sample version 20 db tables
   1170         createSampleDb(db, 20);
   1171         db.close();
   1172 
   1173         // upgrade database 20 to latest
   1174         LocalStore.newInstance(mLocalStoreUri, getContext(), null);
   1175 
   1176         // database should be upgraded
   1177         db = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
   1178         assertEquals("database should be upgraded", DATABASE_VERSION, db.getVersion());
   1179 
   1180         // check for all "latest version" tables
   1181         checkAllTablesFound(db);
   1182     }
   1183 
   1184     /**
   1185      * Check upgrade from db version 21 to latest
   1186      */
   1187     public void testDbUpgrade21ToLatest() throws MessagingException, URISyntaxException {
   1188         final URI uri = new URI(mLocalStoreUri);
   1189         final String dbPath = uri.getPath();
   1190         SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
   1191 
   1192         // create sample version 21 db tables
   1193         createSampleDb(db, 21);
   1194         db.close();
   1195 
   1196         // upgrade database 21 to latest
   1197         LocalStore.newInstance(mLocalStoreUri, getContext(), null);
   1198 
   1199         // database should be upgraded
   1200         db = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
   1201         assertEquals("database should be upgraded", DATABASE_VERSION, db.getVersion());
   1202 
   1203         // check for all "latest version" tables
   1204         checkAllTablesFound(db);
   1205     }
   1206 
   1207     /**
   1208      * Check upgrade from db version 22 to latest.
   1209      * Flags must be migrated to new columns.
   1210      */
   1211     public void testDbUpgrade22ToLatest() throws MessagingException, URISyntaxException {
   1212         final URI uri = new URI(mLocalStoreUri);
   1213         final String dbPath = uri.getPath();
   1214         SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
   1215 
   1216         // create sample version 22 db tables
   1217         createSampleDb(db, 22);
   1218 
   1219         // insert three messages, one for each migration flag
   1220         final ContentValues inMessage1 = new ContentValues();
   1221         inMessage1.put("message_id", (String) "x"); // message_id text == String
   1222         inMessage1.put("flags", Flag.X_DOWNLOADED_FULL.toString());
   1223         final ContentValues outMessage1 = new ContentValues(inMessage1);
   1224         outMessage1.put("id", db.insert("messages", null, inMessage1));
   1225 
   1226         final ContentValues inMessage2 = new ContentValues();
   1227         inMessage2.put("message_id", (String) "y"); // message_id text == String
   1228         inMessage2.put("flags", Flag.X_DOWNLOADED_PARTIAL.toString());
   1229         final ContentValues outMessage2 = new ContentValues(inMessage2);
   1230         outMessage2.put("id", db.insert("messages", null, inMessage2));
   1231 
   1232         final ContentValues inMessage3 = new ContentValues();
   1233         inMessage3.put("message_id", (String) "z"); // message_id text == String
   1234         inMessage3.put("flags", Flag.DELETED.toString());
   1235         final ContentValues outMessage3 = new ContentValues(inMessage3);
   1236         outMessage3.put("id", db.insert("messages", null, inMessage3));
   1237 
   1238         db.close();
   1239 
   1240         // upgrade database 22 to latest
   1241         LocalStore.newInstance(mLocalStoreUri, getContext(), null);
   1242 
   1243         // database should be upgraded
   1244         db = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
   1245         assertEquals("database should be upgraded", DATABASE_VERSION, db.getVersion());
   1246 
   1247         // check for all "latest version" tables
   1248         checkAllTablesFound(db);
   1249 
   1250         // check message table for migrated flags
   1251         String[] columns = new String[] { "id", "message_id", "flags",
   1252                 "flag_downloaded_full", "flag_downloaded_partial", "flag_deleted" };
   1253         Cursor c = db.query("messages", columns, null, null, null, null, null);
   1254 
   1255         for (int msgNum = 0; msgNum <= 2; ++msgNum) {
   1256             assertTrue(c.moveToNext());
   1257             ContentValues actualMessage = cursorToContentValues(c,
   1258                     new String[] { "primary", "text", "text", "integer", "integer", "integer" });
   1259             String messageId = actualMessage.getAsString("message_id");
   1260             int outDlFull = actualMessage.getAsInteger("flag_downloaded_full");
   1261             int outDlPartial = actualMessage.getAsInteger("flag_downloaded_partial");
   1262             int outDeleted = actualMessage.getAsInteger("flag_deleted");
   1263             if ("x".equals(messageId)) {
   1264                 assertTrue("converted flag_downloaded_full",
   1265                         outDlFull == 1 && outDlPartial == 0 && outDeleted == 0);
   1266             } else if ("y".equals(messageId)) {
   1267                 assertTrue("converted flag_downloaded_partial",
   1268                         outDlFull == 0 && outDlPartial == 1 && outDeleted == 0);
   1269             } else if ("z".equals(messageId)) {
   1270                 assertTrue("converted flag_deleted",
   1271                         outDlFull == 0 && outDlPartial == 0 && outDeleted == 1);
   1272             }
   1273         }
   1274         c.close();
   1275     }
   1276 
   1277     /**
   1278      * Tests for database upgrade from version 23 to current version.
   1279      */
   1280     public void testDbUpgrade23ToLatest() throws MessagingException, URISyntaxException {
   1281         final URI uri = new URI(mLocalStoreUri);
   1282         final String dbPath = uri.getPath();
   1283         SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
   1284 
   1285         // create sample version 23 db tables
   1286         createSampleDb(db, 23);
   1287 
   1288         // sample message data and expected data
   1289         final ContentValues initialMessage = new ContentValues();
   1290         initialMessage.put("folder_id", (long) 2);        // folder_id type integer == Long
   1291         initialMessage.put("internal_date", (long) 3);    // internal_date type integer == Long
   1292         final ContentValues expectedMessage = new ContentValues(initialMessage);
   1293         expectedMessage.put("id", db.insert("messages", null, initialMessage));
   1294 
   1295         db.close();
   1296 
   1297         // upgrade database 23 to latest
   1298         LocalStore.newInstance(mLocalStoreUri, getContext(), null);
   1299 
   1300         // added message_id column should be initialized as null
   1301         expectedMessage.put("message_id", (String) null);    // message_id type text == String
   1302 
   1303         // database should be upgraded
   1304         db = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
   1305         assertEquals("database should be upgraded", DATABASE_VERSION, db.getVersion());
   1306         Cursor c;
   1307 
   1308         // check for all "latest version" tables
   1309         checkAllTablesFound(db);
   1310 
   1311         // check message table
   1312         c = db.query("messages",
   1313                 new String[] { "id", "folder_id", "internal_date", "message_id" },
   1314                 null, null, null, null, null);
   1315         // check if data is available
   1316         assertTrue("messages table should have one data", c.moveToNext());
   1317 
   1318         // check if data are expected
   1319         final ContentValues actualMessage = cursorToContentValues(c,
   1320                 new String[] { "primary", "integer", "integer", "text" });
   1321         assertEquals("messages table cursor does not have expected values",
   1322                 expectedMessage, actualMessage);
   1323         c.close();
   1324 
   1325         db.close();
   1326     }
   1327 
   1328    /**
   1329      * Checks the database to confirm that all tables, with all expected columns are found.
   1330      */
   1331     private void checkAllTablesFound(SQLiteDatabase db) {
   1332         Cursor c;
   1333         HashSet<String> foundNames;
   1334         ArrayList<String> expectedNames;
   1335 
   1336         // check for up-to-date messages table
   1337         c = db.query("messages",
   1338                 null,
   1339                 null, null, null, null, null);
   1340         foundNames = cursorToColumnNames(c);
   1341         expectedNames = new ArrayList<String>(Arrays.asList(
   1342                 new String[]{ "id", "folder_id", "uid", "subject", "date", "flags", "sender_list",
   1343                         "to_list", "cc_list", "bcc_list", "reply_to_list",
   1344                         "html_content", "text_content", "attachment_count",
   1345                         "internal_date", "store_flag_1", "store_flag_2", "flag_downloaded_full",
   1346                         "flag_downloaded_partial", "flag_deleted", "x_headers" }
   1347                 ));
   1348         assertTrue("messages", foundNames.containsAll(expectedNames));
   1349 
   1350         // check for up-to-date attachments table
   1351         c = db.query("attachments",
   1352                 null,
   1353                 null, null, null, null, null);
   1354         foundNames = cursorToColumnNames(c);
   1355         expectedNames = new ArrayList<String>(Arrays.asList(
   1356                 new String[]{ "id", "message_id",
   1357                         "store_data", "content_uri", "size", "name",
   1358                         "mime_type", "content_id" }
   1359                 ));
   1360         assertTrue("attachments", foundNames.containsAll(expectedNames));
   1361 
   1362         // check for up-to-date remote_store_data table
   1363         c = db.query("remote_store_data",
   1364                 null,
   1365                 null, null, null, null, null);
   1366         foundNames = cursorToColumnNames(c);
   1367         expectedNames = new ArrayList<String>(Arrays.asList(
   1368                 new String[]{ "id", "folder_id", "data_key", "data" }
   1369                 ));
   1370         assertTrue("remote_store_data", foundNames.containsAll(expectedNames));
   1371     }
   1372 
   1373     private static void createSampleDb(SQLiteDatabase db, int version) {
   1374         db.execSQL("DROP TABLE IF EXISTS messages");
   1375         db.execSQL("CREATE TABLE messages (id INTEGER PRIMARY KEY, folder_id INTEGER, " +
   1376                    "uid TEXT, subject TEXT, date INTEGER, flags TEXT, sender_list TEXT, " +
   1377                    "to_list TEXT, cc_list TEXT, bcc_list TEXT, reply_to_list TEXT, " +
   1378                    "html_content TEXT, text_content TEXT, attachment_count INTEGER, " +
   1379                    "internal_date INTEGER" +
   1380                    ((version >= 19) ? ", message_id TEXT" : "") +
   1381                    ((version >= 22) ? ", store_flag_1 INTEGER, store_flag_2 INTEGER" : "") +
   1382                    ((version >= 23) ?
   1383                            ", flag_downloaded_full INTEGER, flag_downloaded_partial INTEGER" : "") +
   1384                    ((version >= 23) ? ", flag_deleted INTEGER" : "") +
   1385                    ((version >= 24) ? ", x_headers TEXT" : "") +
   1386                    ")");
   1387         db.execSQL("DROP TABLE IF EXISTS attachments");
   1388         db.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER," +
   1389                    "store_data TEXT, content_uri TEXT, size INTEGER, name TEXT," +
   1390                    "mime_type TEXT" +
   1391                    ((version >= 20) ? ", content_id" : "") +
   1392                    ")");
   1393 
   1394         if (version >= 21) {
   1395             db.execSQL("DROP TABLE IF EXISTS remote_store_data");
   1396             db.execSQL("CREATE TABLE remote_store_data "
   1397                     + "(id INTEGER PRIMARY KEY, folder_id INTEGER, "
   1398                     + "data_key TEXT, data TEXT)");
   1399         }
   1400 
   1401         db.setVersion(version);
   1402     }
   1403 }
   1404