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