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