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