EmailProvider.java revision 9a01353f14f718c5447d6a3dc25b413be0f175d9
1/* 2 * Copyright (C) 2009 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 17package com.android.email.provider; 18 19import android.accounts.AccountManager; 20import android.content.ContentProvider; 21import android.content.ContentProviderOperation; 22import android.content.ContentProviderResult; 23import android.content.ContentResolver; 24import android.content.ContentUris; 25import android.content.ContentValues; 26import android.content.Context; 27import android.content.OperationApplicationException; 28import android.content.UriMatcher; 29import android.database.ContentObserver; 30import android.database.Cursor; 31import android.database.MatrixCursor; 32import android.database.SQLException; 33import android.database.sqlite.SQLiteDatabase; 34import android.database.sqlite.SQLiteException; 35import android.database.sqlite.SQLiteOpenHelper; 36import android.net.Uri; 37import android.provider.ContactsContract; 38import android.text.TextUtils; 39import android.util.Log; 40 41import com.android.email.Email; 42import com.android.email.Preferences; 43import com.android.email.provider.ContentCache.CacheToken; 44import com.android.email.service.AttachmentDownloadService; 45import com.android.emailcommon.AccountManagerTypes; 46import com.android.emailcommon.CalendarProviderStub; 47import com.android.emailcommon.Logging; 48import com.android.emailcommon.provider.Account; 49import com.android.emailcommon.provider.EmailContent; 50import com.android.emailcommon.provider.EmailContent.AccountColumns; 51import com.android.emailcommon.provider.EmailContent.Attachment; 52import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 53import com.android.emailcommon.provider.EmailContent.Body; 54import com.android.emailcommon.provider.EmailContent.BodyColumns; 55import com.android.emailcommon.provider.EmailContent.HostAuthColumns; 56import com.android.emailcommon.provider.EmailContent.MailboxColumns; 57import com.android.emailcommon.provider.EmailContent.Message; 58import com.android.emailcommon.provider.EmailContent.MessageColumns; 59import com.android.emailcommon.provider.EmailContent.PolicyColumns; 60import com.android.emailcommon.provider.EmailContent.QuickResponseColumns; 61import com.android.emailcommon.provider.EmailContent.SyncColumns; 62import com.android.emailcommon.provider.HostAuth; 63import com.android.emailcommon.provider.Mailbox; 64import com.android.emailcommon.provider.Policy; 65import com.android.emailcommon.provider.QuickResponse; 66import com.android.emailcommon.service.LegacyPolicySet; 67import com.google.common.annotations.VisibleForTesting; 68 69import java.io.File; 70import java.util.ArrayList; 71import java.util.Arrays; 72import java.util.Collection; 73import java.util.HashMap; 74import java.util.Map; 75 76public class EmailProvider extends ContentProvider { 77 78 private static final String TAG = "EmailProvider"; 79 80 protected static final String DATABASE_NAME = "EmailProvider.db"; 81 protected static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; 82 protected static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db"; 83 84 public static final String ACTION_ATTACHMENT_UPDATED = "com.android.email.ATTACHMENT_UPDATED"; 85 public static final String ATTACHMENT_UPDATED_EXTRA_FLAGS = 86 "com.android.email.ATTACHMENT_UPDATED_FLAGS"; 87 88 public static final String EMAIL_MESSAGE_MIME_TYPE = 89 "vnd.android.cursor.item/email-message"; 90 public static final String EMAIL_ATTACHMENT_MIME_TYPE = 91 "vnd.android.cursor.item/email-attachment"; 92 93 public static final Uri INTEGRITY_CHECK_URI = 94 Uri.parse("content://" + EmailContent.AUTHORITY + "/integrityCheck"); 95 public static final Uri ACCOUNT_BACKUP_URI = 96 Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup"); 97 98 /** Appended to the notification URI for delete operations */ 99 public static final String NOTIFICATION_OP_DELETE = "delete"; 100 /** Appended to the notification URI for insert operations */ 101 public static final String NOTIFICATION_OP_INSERT = "insert"; 102 /** Appended to the notification URI for update operations */ 103 public static final String NOTIFICATION_OP_UPDATE = "update"; 104 105 // Definitions for our queries looking for orphaned messages 106 private static final String[] ORPHANS_PROJECTION 107 = new String[] {MessageColumns.ID, MessageColumns.MAILBOX_KEY}; 108 private static final int ORPHANS_ID = 0; 109 private static final int ORPHANS_MAILBOX_KEY = 1; 110 111 private static final String WHERE_ID = EmailContent.RECORD_ID + "=?"; 112 113 // This is not a hard limit on accounts, per se, but beyond this, we can't guarantee that all 114 // critical mailboxes, host auth's, accounts, and policies are cached 115 private static final int MAX_CACHED_ACCOUNTS = 16; 116 // Inbox, Drafts, Sent, Outbox, Trash, and Search (these boxes are cached when possible) 117 private static final int NUM_ALWAYS_CACHED_MAILBOXES = 6; 118 119 // We'll cache the following four tables; sizes are best estimates of effective values 120 private final ContentCache mCacheAccount = 121 new ContentCache("Account", Account.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS); 122 private final ContentCache mCacheHostAuth = 123 new ContentCache("HostAuth", HostAuth.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS * 2); 124 /*package*/ final ContentCache mCacheMailbox = 125 new ContentCache("Mailbox", Mailbox.CONTENT_PROJECTION, 126 MAX_CACHED_ACCOUNTS * (NUM_ALWAYS_CACHED_MAILBOXES + 2)); 127 private final ContentCache mCacheMessage = 128 new ContentCache("Message", Message.CONTENT_PROJECTION, 8); 129 private final ContentCache mCachePolicy = 130 new ContentCache("Policy", Policy.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS); 131 132 // Any changes to the database format *must* include update-in-place code. 133 // Original version: 3 134 // Version 4: Database wipe required; changing AccountManager interface w/Exchange 135 // Version 5: Database wipe required; changing AccountManager interface w/Exchange 136 // Version 6: Adding Message.mServerTimeStamp column 137 // Version 7: Replace the mailbox_delete trigger with a version that removes orphaned messages 138 // from the Message_Deletes and Message_Updates tables 139 // Version 8: Add security flags column to accounts table 140 // Version 9: Add security sync key and signature to accounts table 141 // Version 10: Add meeting info to message table 142 // Version 11: Add content and flags to attachment table 143 // Version 12: Add content_bytes to attachment table. content is deprecated. 144 // Version 13: Add messageCount to Mailbox table. 145 // Version 14: Add snippet to Message table 146 // Version 15: Fix upgrade problem in version 14. 147 // Version 16: Add accountKey to Attachment table 148 // Version 17: Add parentKey to Mailbox table 149 // Version 18: Copy Mailbox.displayName to Mailbox.serverId for all IMAP & POP3 mailboxes. 150 // Column Mailbox.serverId is used for the server-side pathname of a mailbox. 151 // Version 19: Add Policy table; add policyKey to Account table and trigger to delete an 152 // Account's policy when the Account is deleted 153 // Version 20: Add new policies to Policy table 154 // Version 21: Add lastSeenMessageKey column to Mailbox table 155 // Version 22: Upgrade path for IMAP/POP accounts to integrate with AccountManager 156 // Version 23: Add column to mailbox table for time of last access 157 // Version 24: Add column to hostauth table for client cert alias 158 // Version 25: Added QuickResponse table 159 // Version 26: Update IMAP accounts to add FLAG_SUPPORTS_SEARCH flag 160 // Version 27: Add protocolSearchInfo to Message table 161 162 public static final int DATABASE_VERSION = 27; 163 164 // Any changes to the database format *must* include update-in-place code. 165 // Original version: 2 166 // Version 3: Add "sourceKey" column 167 // Version 4: Database wipe required; changing AccountManager interface w/Exchange 168 // Version 5: Database wipe required; changing AccountManager interface w/Exchange 169 // Version 6: Adding Body.mIntroText column 170 public static final int BODY_DATABASE_VERSION = 6; 171 172 private static final int ACCOUNT_BASE = 0; 173 private static final int ACCOUNT = ACCOUNT_BASE; 174 private static final int ACCOUNT_ID = ACCOUNT_BASE + 1; 175 private static final int ACCOUNT_ID_ADD_TO_FIELD = ACCOUNT_BASE + 2; 176 private static final int ACCOUNT_RESET_NEW_COUNT = ACCOUNT_BASE + 3; 177 private static final int ACCOUNT_RESET_NEW_COUNT_ID = ACCOUNT_BASE + 4; 178 private static final int ACCOUNT_DEFAULT_ID = ACCOUNT_BASE + 5; 179 180 private static final int MAILBOX_BASE = 0x1000; 181 private static final int MAILBOX = MAILBOX_BASE; 182 private static final int MAILBOX_ID = MAILBOX_BASE + 1; 183 private static final int MAILBOX_ID_FROM_ACCOUNT_AND_TYPE = MAILBOX_BASE + 2; 184 private static final int MAILBOX_ID_ADD_TO_FIELD = MAILBOX_BASE + 2; 185 186 private static final int MESSAGE_BASE = 0x2000; 187 private static final int MESSAGE = MESSAGE_BASE; 188 private static final int MESSAGE_ID = MESSAGE_BASE + 1; 189 private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2; 190 191 private static final int ATTACHMENT_BASE = 0x3000; 192 private static final int ATTACHMENT = ATTACHMENT_BASE; 193 private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1; 194 private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2; 195 196 private static final int HOSTAUTH_BASE = 0x4000; 197 private static final int HOSTAUTH = HOSTAUTH_BASE; 198 private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1; 199 200 private static final int UPDATED_MESSAGE_BASE = 0x5000; 201 private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE; 202 private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1; 203 204 private static final int DELETED_MESSAGE_BASE = 0x6000; 205 private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE; 206 private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1; 207 208 private static final int POLICY_BASE = 0x7000; 209 private static final int POLICY = POLICY_BASE; 210 private static final int POLICY_ID = POLICY_BASE + 1; 211 212 private static final int QUICK_RESPONSE_BASE = 0x8000; 213 private static final int QUICK_RESPONSE = QUICK_RESPONSE_BASE; 214 private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1; 215 private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2; 216 217 // MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS 218 private static final int LAST_EMAIL_PROVIDER_DB_BASE = QUICK_RESPONSE_BASE; 219 220 // DO NOT CHANGE BODY_BASE!! 221 private static final int BODY_BASE = LAST_EMAIL_PROVIDER_DB_BASE + 0x1000; 222 private static final int BODY = BODY_BASE; 223 private static final int BODY_ID = BODY_BASE + 1; 224 225 private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc. 226 227 // TABLE_NAMES MUST remain in the order of the BASE constants above (e.g. ACCOUNT_BASE = 0x0000, 228 // MESSAGE_BASE = 0x1000, etc.) 229 private static final String[] TABLE_NAMES = { 230 Account.TABLE_NAME, 231 Mailbox.TABLE_NAME, 232 Message.TABLE_NAME, 233 Attachment.TABLE_NAME, 234 HostAuth.TABLE_NAME, 235 Message.UPDATED_TABLE_NAME, 236 Message.DELETED_TABLE_NAME, 237 Policy.TABLE_NAME, 238 QuickResponse.TABLE_NAME, 239 Body.TABLE_NAME 240 }; 241 242 // CONTENT_CACHES MUST remain in the order of the BASE constants above 243 private final ContentCache[] mContentCaches = { 244 mCacheAccount, 245 mCacheMailbox, 246 mCacheMessage, 247 null, // Attachment 248 mCacheHostAuth, 249 null, // Updated message 250 null, // Deleted message 251 mCachePolicy, 252 null, // Quick response 253 null // Body 254 }; 255 256 // CACHE_PROJECTIONS MUST remain in the order of the BASE constants above 257 private static final String[][] CACHE_PROJECTIONS = { 258 Account.CONTENT_PROJECTION, 259 Mailbox.CONTENT_PROJECTION, 260 Message.CONTENT_PROJECTION, 261 null, // Attachment 262 HostAuth.CONTENT_PROJECTION, 263 null, // Updated message 264 null, // Deleted message 265 Policy.CONTENT_PROJECTION, 266 null, // Quick response 267 null // Body 268 }; 269 270 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 271 272 private static final String MAILBOX_PRE_CACHE_SELECTION = MailboxColumns.TYPE + " IN (" + 273 Mailbox.TYPE_INBOX + "," + Mailbox.TYPE_DRAFTS + "," + Mailbox.TYPE_TRASH + "," + 274 Mailbox.TYPE_SENT + "," + Mailbox.TYPE_SEARCH + "," + Mailbox.TYPE_OUTBOX + ")"; 275 276 /** 277 * Let's only generate these SQL strings once, as they are used frequently 278 * Note that this isn't relevant for table creation strings, since they are used only once 279 */ 280 private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " + 281 Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 282 EmailContent.RECORD_ID + '='; 283 284 private static final String UPDATED_MESSAGE_DELETE = "delete from " + 285 Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '='; 286 287 private static final String DELETED_MESSAGE_INSERT = "insert or replace into " + 288 Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 289 EmailContent.RECORD_ID + '='; 290 291 private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME + 292 " where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY + 293 " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " + 294 Message.TABLE_NAME + ')'; 295 296 private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME + 297 " where " + BodyColumns.MESSAGE_KEY + '='; 298 299 private static final String ID_EQUALS = EmailContent.RECORD_ID + "=?"; 300 301 private static final String TRIGGER_MAILBOX_DELETE = 302 "create trigger mailbox_delete before delete on " + Mailbox.TABLE_NAME + 303 " begin" + 304 " delete from " + Message.TABLE_NAME + 305 " where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID + 306 "; delete from " + Message.UPDATED_TABLE_NAME + 307 " where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID + 308 "; delete from " + Message.DELETED_TABLE_NAME + 309 " where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID + 310 "; end"; 311 312 private static final String TRIGGER_ACCOUNT_DELETE = 313 "create trigger account_delete before delete on " + Account.TABLE_NAME + 314 " begin delete from " + Mailbox.TABLE_NAME + 315 " where " + MailboxColumns.ACCOUNT_KEY + "=old." + EmailContent.RECORD_ID + 316 "; delete from " + HostAuth.TABLE_NAME + 317 " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_RECV + 318 "; delete from " + HostAuth.TABLE_NAME + 319 " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_SEND + 320 "; delete from " + Policy.TABLE_NAME + 321 " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.POLICY_KEY + 322 "; end"; 323 324 private static final ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 325 326 public static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; 327 328 static { 329 // Email URI matching table 330 UriMatcher matcher = sURIMatcher; 331 332 // All accounts 333 matcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT); 334 // A specific account 335 // insert into this URI causes a mailbox to be added to the account 336 matcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID); 337 matcher.addURI(EmailContent.AUTHORITY, "account/default", ACCOUNT_DEFAULT_ID); 338 339 // Special URI to reset the new message count. Only update works, and content values 340 // will be ignored. 341 matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount", 342 ACCOUNT_RESET_NEW_COUNT); 343 matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount/#", 344 ACCOUNT_RESET_NEW_COUNT_ID); 345 346 // All mailboxes 347 matcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX); 348 // A specific mailbox 349 // insert into this URI causes a message to be added to the mailbox 350 // ** NOTE For now, the accountKey must be set manually in the values! 351 matcher.addURI(EmailContent.AUTHORITY, "mailbox/#", MAILBOX_ID); 352 matcher.addURI(EmailContent.AUTHORITY, "mailboxIdFromAccountAndType/#/#", 353 MAILBOX_ID_FROM_ACCOUNT_AND_TYPE); 354 // All messages 355 matcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE); 356 // A specific message 357 // insert into this URI causes an attachment to be added to the message 358 matcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID); 359 360 // A specific attachment 361 matcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT); 362 // A specific attachment (the header information) 363 matcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID); 364 // The attachments of a specific message (query only) (insert & delete TBD) 365 matcher.addURI(EmailContent.AUTHORITY, "attachment/message/#", 366 ATTACHMENTS_MESSAGE_ID); 367 368 // All mail bodies 369 matcher.addURI(EmailContent.AUTHORITY, "body", BODY); 370 // A specific mail body 371 matcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID); 372 373 // All hostauth records 374 matcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH); 375 // A specific hostauth 376 matcher.addURI(EmailContent.AUTHORITY, "hostauth/#", HOSTAUTH_ID); 377 378 // Atomically a constant value to a particular field of a mailbox/account 379 matcher.addURI(EmailContent.AUTHORITY, "mailboxIdAddToField/#", 380 MAILBOX_ID_ADD_TO_FIELD); 381 matcher.addURI(EmailContent.AUTHORITY, "accountIdAddToField/#", 382 ACCOUNT_ID_ADD_TO_FIELD); 383 384 /** 385 * THIS URI HAS SPECIAL SEMANTICS 386 * ITS USE IS INTENDED FOR THE UI APPLICATION TO MARK CHANGES THAT NEED TO BE SYNCED BACK 387 * TO A SERVER VIA A SYNC ADAPTER 388 */ 389 matcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); 390 391 /** 392 * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY 393 * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI 394 * BY THE UI APPLICATION 395 */ 396 // All deleted messages 397 matcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE); 398 // A specific deleted message 399 matcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID); 400 401 // All updated messages 402 matcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE); 403 // A specific updated message 404 matcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID); 405 406 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues(); 407 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0); 408 409 matcher.addURI(EmailContent.AUTHORITY, "policy", POLICY); 410 matcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID); 411 412 // All quick responses 413 matcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE); 414 // A specific quick response 415 matcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID); 416 // All quick responses associated with a particular account id 417 matcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#", 418 QUICK_RESPONSE_ACCOUNT_ID); 419 } 420 421 /** 422 * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in 423 * @param uri the Uri to match 424 * @return the match value 425 */ 426 private static int findMatch(Uri uri, String methodName) { 427 int match = sURIMatcher.match(uri); 428 if (match < 0) { 429 throw new IllegalArgumentException("Unknown uri: " + uri); 430 } else if (Logging.LOGD) { 431 Log.v(TAG, methodName + ": uri=" + uri + ", match is " + match); 432 } 433 return match; 434 } 435 436 /* 437 * Internal helper method for index creation. 438 * Example: 439 * "create index message_" + MessageColumns.FLAG_READ 440 * + " on " + Message.TABLE_NAME + " (" + MessageColumns.FLAG_READ + ");" 441 */ 442 /* package */ 443 static String createIndex(String tableName, String columnName) { 444 return "create index " + tableName.toLowerCase() + '_' + columnName 445 + " on " + tableName + " (" + columnName + ");"; 446 } 447 448 static void createMessageTable(SQLiteDatabase db) { 449 String messageColumns = MessageColumns.DISPLAY_NAME + " text, " 450 + MessageColumns.TIMESTAMP + " integer, " 451 + MessageColumns.SUBJECT + " text, " 452 + MessageColumns.FLAG_READ + " integer, " 453 + MessageColumns.FLAG_LOADED + " integer, " 454 + MessageColumns.FLAG_FAVORITE + " integer, " 455 + MessageColumns.FLAG_ATTACHMENT + " integer, " 456 + MessageColumns.FLAGS + " integer, " 457 + MessageColumns.CLIENT_ID + " integer, " 458 + MessageColumns.MESSAGE_ID + " text, " 459 + MessageColumns.MAILBOX_KEY + " integer, " 460 + MessageColumns.ACCOUNT_KEY + " integer, " 461 + MessageColumns.FROM_LIST + " text, " 462 + MessageColumns.TO_LIST + " text, " 463 + MessageColumns.CC_LIST + " text, " 464 + MessageColumns.BCC_LIST + " text, " 465 + MessageColumns.REPLY_TO_LIST + " text, " 466 + MessageColumns.MEETING_INFO + " text, " 467 + MessageColumns.SNIPPET + " text, " 468 + MessageColumns.PROTOCOL_SEARCH_INFO + " text" 469 + ");"; 470 471 // This String and the following String MUST have the same columns, except for the type 472 // of those columns! 473 String createString = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 474 + SyncColumns.SERVER_ID + " text, " 475 + SyncColumns.SERVER_TIMESTAMP + " integer, " 476 + messageColumns; 477 478 // For the updated and deleted tables, the id is assigned, but we do want to keep track 479 // of the ORDER of updates using an autoincrement primary key. We use the DATA column 480 // at this point; it has no other function 481 String altCreateString = " (" + EmailContent.RECORD_ID + " integer unique, " 482 + SyncColumns.SERVER_ID + " text, " 483 + SyncColumns.SERVER_TIMESTAMP + " integer, " 484 + messageColumns; 485 486 // The three tables have the same schema 487 db.execSQL("create table " + Message.TABLE_NAME + createString); 488 db.execSQL("create table " + Message.UPDATED_TABLE_NAME + altCreateString); 489 db.execSQL("create table " + Message.DELETED_TABLE_NAME + altCreateString); 490 491 String indexColumns[] = { 492 MessageColumns.TIMESTAMP, 493 MessageColumns.FLAG_READ, 494 MessageColumns.FLAG_LOADED, 495 MessageColumns.MAILBOX_KEY, 496 SyncColumns.SERVER_ID 497 }; 498 499 for (String columnName : indexColumns) { 500 db.execSQL(createIndex(Message.TABLE_NAME, columnName)); 501 } 502 503 // Deleting a Message deletes all associated Attachments 504 // Deleting the associated Body cannot be done in a trigger, because the Body is stored 505 // in a separate database, and trigger cannot operate on attached databases. 506 db.execSQL("create trigger message_delete before delete on " + Message.TABLE_NAME + 507 " begin delete from " + Attachment.TABLE_NAME + 508 " where " + AttachmentColumns.MESSAGE_KEY + "=old." + EmailContent.RECORD_ID + 509 "; end"); 510 511 // Add triggers to keep unread count accurate per mailbox 512 513 // NOTE: SQLite's before triggers are not safe when recursive triggers are involved. 514 // Use caution when changing them. 515 516 // Insert a message; if flagRead is zero, add to the unread count of the message's mailbox 517 db.execSQL("create trigger unread_message_insert before insert on " + Message.TABLE_NAME + 518 " when NEW." + MessageColumns.FLAG_READ + "=0" + 519 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 520 '=' + MailboxColumns.UNREAD_COUNT + "+1" + 521 " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY + 522 "; end"); 523 524 // Delete a message; if flagRead is zero, decrement the unread count of the msg's mailbox 525 db.execSQL("create trigger unread_message_delete before delete on " + Message.TABLE_NAME + 526 " when OLD." + MessageColumns.FLAG_READ + "=0" + 527 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 528 '=' + MailboxColumns.UNREAD_COUNT + "-1" + 529 " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY + 530 "; end"); 531 532 // Change a message's mailbox 533 db.execSQL("create trigger unread_message_move before update of " + 534 MessageColumns.MAILBOX_KEY + " on " + Message.TABLE_NAME + 535 " when OLD." + MessageColumns.FLAG_READ + "=0" + 536 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 537 '=' + MailboxColumns.UNREAD_COUNT + "-1" + 538 " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY + 539 "; update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 540 '=' + MailboxColumns.UNREAD_COUNT + "+1" + 541 " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY + 542 "; end"); 543 544 // Change a message's read state 545 db.execSQL("create trigger unread_message_read before update of " + 546 MessageColumns.FLAG_READ + " on " + Message.TABLE_NAME + 547 " when OLD." + MessageColumns.FLAG_READ + "!=NEW." + MessageColumns.FLAG_READ + 548 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 549 '=' + MailboxColumns.UNREAD_COUNT + "+ case OLD." + MessageColumns.FLAG_READ + 550 " when 0 then -1 else 1 end" + 551 " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY + 552 "; end"); 553 554 // Add triggers to update message count per mailbox 555 556 // Insert a message. 557 db.execSQL("create trigger message_count_message_insert after insert on " + 558 Message.TABLE_NAME + 559 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT + 560 '=' + MailboxColumns.MESSAGE_COUNT + "+1" + 561 " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY + 562 "; end"); 563 564 // Delete a message; if flagRead is zero, decrement the unread count of the msg's mailbox 565 db.execSQL("create trigger message_count_message_delete after delete on " + 566 Message.TABLE_NAME + 567 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT + 568 '=' + MailboxColumns.MESSAGE_COUNT + "-1" + 569 " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY + 570 "; end"); 571 572 // Change a message's mailbox 573 db.execSQL("create trigger message_count_message_move after update of " + 574 MessageColumns.MAILBOX_KEY + " on " + Message.TABLE_NAME + 575 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT + 576 '=' + MailboxColumns.MESSAGE_COUNT + "-1" + 577 " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY + 578 "; update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT + 579 '=' + MailboxColumns.MESSAGE_COUNT + "+1" + 580 " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY + 581 "; end"); 582 } 583 584 static void resetMessageTable(SQLiteDatabase db, int oldVersion, int newVersion) { 585 try { 586 db.execSQL("drop table " + Message.TABLE_NAME); 587 db.execSQL("drop table " + Message.UPDATED_TABLE_NAME); 588 db.execSQL("drop table " + Message.DELETED_TABLE_NAME); 589 } catch (SQLException e) { 590 } 591 createMessageTable(db); 592 } 593 594 @SuppressWarnings("deprecation") 595 static void createAccountTable(SQLiteDatabase db) { 596 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 597 + AccountColumns.DISPLAY_NAME + " text, " 598 + AccountColumns.EMAIL_ADDRESS + " text, " 599 + AccountColumns.SYNC_KEY + " text, " 600 + AccountColumns.SYNC_LOOKBACK + " integer, " 601 + AccountColumns.SYNC_INTERVAL + " text, " 602 + AccountColumns.HOST_AUTH_KEY_RECV + " integer, " 603 + AccountColumns.HOST_AUTH_KEY_SEND + " integer, " 604 + AccountColumns.FLAGS + " integer, " 605 + AccountColumns.IS_DEFAULT + " integer, " 606 + AccountColumns.COMPATIBILITY_UUID + " text, " 607 + AccountColumns.SENDER_NAME + " text, " 608 + AccountColumns.RINGTONE_URI + " text, " 609 + AccountColumns.PROTOCOL_VERSION + " text, " 610 + AccountColumns.NEW_MESSAGE_COUNT + " integer, " 611 + AccountColumns.SECURITY_FLAGS + " integer, " 612 + AccountColumns.SECURITY_SYNC_KEY + " text, " 613 + AccountColumns.SIGNATURE + " text, " 614 + AccountColumns.POLICY_KEY + " integer" 615 + ");"; 616 db.execSQL("create table " + Account.TABLE_NAME + s); 617 // Deleting an account deletes associated Mailboxes and HostAuth's 618 db.execSQL(TRIGGER_ACCOUNT_DELETE); 619 } 620 621 static void resetAccountTable(SQLiteDatabase db, int oldVersion, int newVersion) { 622 try { 623 db.execSQL("drop table " + Account.TABLE_NAME); 624 } catch (SQLException e) { 625 } 626 createAccountTable(db); 627 } 628 629 static void createPolicyTable(SQLiteDatabase db) { 630 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 631 + PolicyColumns.PASSWORD_MODE + " integer, " 632 + PolicyColumns.PASSWORD_MIN_LENGTH + " integer, " 633 + PolicyColumns.PASSWORD_EXPIRATION_DAYS + " integer, " 634 + PolicyColumns.PASSWORD_HISTORY + " integer, " 635 + PolicyColumns.PASSWORD_COMPLEX_CHARS + " integer, " 636 + PolicyColumns.PASSWORD_MAX_FAILS + " integer, " 637 + PolicyColumns.MAX_SCREEN_LOCK_TIME + " integer, " 638 + PolicyColumns.REQUIRE_REMOTE_WIPE + " integer, " 639 + PolicyColumns.REQUIRE_ENCRYPTION + " integer, " 640 + PolicyColumns.REQUIRE_ENCRYPTION_EXTERNAL + " integer, " 641 + PolicyColumns.REQUIRE_MANUAL_SYNC_WHEN_ROAMING + " integer, " 642 + PolicyColumns.DONT_ALLOW_CAMERA + " integer, " 643 + PolicyColumns.DONT_ALLOW_ATTACHMENTS + " integer, " 644 + PolicyColumns.DONT_ALLOW_HTML + " integer, " 645 + PolicyColumns.MAX_ATTACHMENT_SIZE + " integer, " 646 + PolicyColumns.MAX_TEXT_TRUNCATION_SIZE + " integer, " 647 + PolicyColumns.MAX_HTML_TRUNCATION_SIZE + " integer, " 648 + PolicyColumns.MAX_EMAIL_LOOKBACK + " integer, " 649 + PolicyColumns.MAX_CALENDAR_LOOKBACK + " integer, " 650 + PolicyColumns.PASSWORD_RECOVERY_ENABLED + " integer" 651 + ");"; 652 db.execSQL("create table " + Policy.TABLE_NAME + s); 653 } 654 655 static void createHostAuthTable(SQLiteDatabase db) { 656 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 657 + HostAuthColumns.PROTOCOL + " text, " 658 + HostAuthColumns.ADDRESS + " text, " 659 + HostAuthColumns.PORT + " integer, " 660 + HostAuthColumns.FLAGS + " integer, " 661 + HostAuthColumns.LOGIN + " text, " 662 + HostAuthColumns.PASSWORD + " text, " 663 + HostAuthColumns.DOMAIN + " text, " 664 + HostAuthColumns.ACCOUNT_KEY + " integer," 665 + HostAuthColumns.CLIENT_CERT_ALIAS + " text" 666 + ");"; 667 db.execSQL("create table " + HostAuth.TABLE_NAME + s); 668 } 669 670 static void resetHostAuthTable(SQLiteDatabase db, int oldVersion, int newVersion) { 671 try { 672 db.execSQL("drop table " + HostAuth.TABLE_NAME); 673 } catch (SQLException e) { 674 } 675 createHostAuthTable(db); 676 } 677 678 static void createMailboxTable(SQLiteDatabase db) { 679 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 680 + MailboxColumns.DISPLAY_NAME + " text, " 681 + MailboxColumns.SERVER_ID + " text, " 682 + MailboxColumns.PARENT_SERVER_ID + " text, " 683 + MailboxColumns.PARENT_KEY + " integer, " 684 + MailboxColumns.ACCOUNT_KEY + " integer, " 685 + MailboxColumns.TYPE + " integer, " 686 + MailboxColumns.DELIMITER + " integer, " 687 + MailboxColumns.SYNC_KEY + " text, " 688 + MailboxColumns.SYNC_LOOKBACK + " integer, " 689 + MailboxColumns.SYNC_INTERVAL + " integer, " 690 + MailboxColumns.SYNC_TIME + " integer, " 691 + MailboxColumns.UNREAD_COUNT + " integer, " 692 + MailboxColumns.FLAG_VISIBLE + " integer, " 693 + MailboxColumns.FLAGS + " integer, " 694 + MailboxColumns.VISIBLE_LIMIT + " integer, " 695 + MailboxColumns.SYNC_STATUS + " text, " 696 + MailboxColumns.MESSAGE_COUNT + " integer not null default 0, " 697 + MailboxColumns.LAST_SEEN_MESSAGE_KEY + " integer, " 698 + MailboxColumns.LAST_TOUCHED_TIME + " integer default 0" 699 + ");"; 700 db.execSQL("create table " + Mailbox.TABLE_NAME + s); 701 db.execSQL("create index mailbox_" + MailboxColumns.SERVER_ID 702 + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.SERVER_ID + ")"); 703 db.execSQL("create index mailbox_" + MailboxColumns.ACCOUNT_KEY 704 + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.ACCOUNT_KEY + ")"); 705 // Deleting a Mailbox deletes associated Messages in all three tables 706 db.execSQL(TRIGGER_MAILBOX_DELETE); 707 } 708 709 static void resetMailboxTable(SQLiteDatabase db, int oldVersion, int newVersion) { 710 try { 711 db.execSQL("drop table " + Mailbox.TABLE_NAME); 712 } catch (SQLException e) { 713 } 714 createMailboxTable(db); 715 } 716 717 static void createAttachmentTable(SQLiteDatabase db) { 718 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 719 + AttachmentColumns.FILENAME + " text, " 720 + AttachmentColumns.MIME_TYPE + " text, " 721 + AttachmentColumns.SIZE + " integer, " 722 + AttachmentColumns.CONTENT_ID + " text, " 723 + AttachmentColumns.CONTENT_URI + " text, " 724 + AttachmentColumns.MESSAGE_KEY + " integer, " 725 + AttachmentColumns.LOCATION + " text, " 726 + AttachmentColumns.ENCODING + " text, " 727 + AttachmentColumns.CONTENT + " text, " 728 + AttachmentColumns.FLAGS + " integer, " 729 + AttachmentColumns.CONTENT_BYTES + " blob, " 730 + AttachmentColumns.ACCOUNT_KEY + " integer" 731 + ");"; 732 db.execSQL("create table " + Attachment.TABLE_NAME + s); 733 db.execSQL(createIndex(Attachment.TABLE_NAME, AttachmentColumns.MESSAGE_KEY)); 734 } 735 736 static void resetAttachmentTable(SQLiteDatabase db, int oldVersion, int newVersion) { 737 try { 738 db.execSQL("drop table " + Attachment.TABLE_NAME); 739 } catch (SQLException e) { 740 } 741 createAttachmentTable(db); 742 } 743 744 static void createQuickResponseTable(SQLiteDatabase db) { 745 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 746 + QuickResponseColumns.TEXT + " text, " 747 + QuickResponseColumns.ACCOUNT_KEY + " integer" 748 + ");"; 749 db.execSQL("create table " + QuickResponse.TABLE_NAME + s); 750 } 751 752 static void createBodyTable(SQLiteDatabase db) { 753 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 754 + BodyColumns.MESSAGE_KEY + " integer, " 755 + BodyColumns.HTML_CONTENT + " text, " 756 + BodyColumns.TEXT_CONTENT + " text, " 757 + BodyColumns.HTML_REPLY + " text, " 758 + BodyColumns.TEXT_REPLY + " text, " 759 + BodyColumns.SOURCE_MESSAGE_KEY + " text, " 760 + BodyColumns.INTRO_TEXT + " text" 761 + ");"; 762 db.execSQL("create table " + Body.TABLE_NAME + s); 763 db.execSQL(createIndex(Body.TABLE_NAME, BodyColumns.MESSAGE_KEY)); 764 } 765 766 static void upgradeBodyTable(SQLiteDatabase db, int oldVersion, int newVersion) { 767 if (oldVersion < 5) { 768 try { 769 db.execSQL("drop table " + Body.TABLE_NAME); 770 createBodyTable(db); 771 } catch (SQLException e) { 772 } 773 } else if (oldVersion == 5) { 774 try { 775 db.execSQL("alter table " + Body.TABLE_NAME 776 + " add " + BodyColumns.INTRO_TEXT + " text"); 777 } catch (SQLException e) { 778 // Shouldn't be needed unless we're debugging and interrupt the process 779 Log.w(TAG, "Exception upgrading EmailProviderBody.db from v5 to v6", e); 780 } 781 oldVersion = 6; 782 } 783 } 784 785 private SQLiteDatabase mDatabase; 786 private SQLiteDatabase mBodyDatabase; 787 788 @VisibleForTesting 789 synchronized SQLiteDatabase getDatabase(Context context) { 790 // Always return the cached database, if we've got one 791 if (mDatabase != null) { 792 return mDatabase; 793 } 794 795 // Whenever we create or re-cache the databases, make sure that we haven't lost one 796 // to corruption 797 checkDatabases(); 798 799 DatabaseHelper helper = new DatabaseHelper(context, DATABASE_NAME); 800 mDatabase = helper.getWritableDatabase(); 801 mDatabase.setLockingEnabled(true); 802 BodyDatabaseHelper bodyHelper = new BodyDatabaseHelper(context, BODY_DATABASE_NAME); 803 mBodyDatabase = bodyHelper.getWritableDatabase(); 804 if (mBodyDatabase != null) { 805 mBodyDatabase.setLockingEnabled(true); 806 String bodyFileName = mBodyDatabase.getPath(); 807 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); 808 } 809 810 // Restore accounts if the database is corrupted... 811 restoreIfNeeded(context, mDatabase); 812 813 // Check for any orphaned Messages in the updated/deleted tables 814 deleteOrphans(mDatabase, Message.UPDATED_TABLE_NAME); 815 deleteOrphans(mDatabase, Message.DELETED_TABLE_NAME); 816 if (Email.DEBUG) { 817 Log.d(TAG, "EmailProvider pre-caching..."); 818 } 819 preCacheData(); 820 if (Email.DEBUG) { 821 Log.d(TAG, "Pre-caching finished."); 822 } 823 return mDatabase; 824 } 825 826 /** 827 * Pre-cache all of the items in a given table meeting the selection criteria 828 * @param tableUri the table uri 829 * @param baseProjection the base projection of that table 830 * @param selection the selection criteria 831 */ 832 private void preCacheTable(Uri tableUri, String[] baseProjection, String selection) { 833 Cursor c = query(tableUri, EmailContent.ID_PROJECTION, selection, null, null); 834 try { 835 while (c.moveToNext()) { 836 long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 837 Cursor cachedCursor = query(ContentUris.withAppendedId( 838 tableUri, id), baseProjection, null, null, null); 839 if (cachedCursor != null) { 840 // For accounts, create a mailbox type map entry (if necessary) 841 if (tableUri == Account.CONTENT_URI) { 842 getOrCreateAccountMailboxTypeMap(id); 843 } 844 cachedCursor.close(); 845 } 846 } 847 } finally { 848 c.close(); 849 } 850 } 851 852 private HashMap<Long, HashMap<Integer, Long>> mMailboxTypeMap = 853 new HashMap<Long, HashMap<Integer, Long>>(); 854 855 private synchronized HashMap<Integer, Long> getOrCreateAccountMailboxTypeMap(long accountId) { 856 HashMap<Integer, Long> accountMailboxTypeMap = mMailboxTypeMap.get(accountId); 857 if (accountMailboxTypeMap == null) { 858 accountMailboxTypeMap = new HashMap<Integer, Long>(); 859 mMailboxTypeMap.put(accountId, accountMailboxTypeMap); 860 } 861 return accountMailboxTypeMap; 862 } 863 864 private synchronized void addToMailboxTypeMap(Cursor c) { 865 long accountId = c.getLong(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN); 866 int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 867 HashMap<Integer, Long> accountMailboxTypeMap = getOrCreateAccountMailboxTypeMap(accountId); 868 accountMailboxTypeMap.put(type, c.getLong(Mailbox.CONTENT_ID_COLUMN)); 869 } 870 871 private void preCacheData() { 872 mMailboxTypeMap.clear(); 873 874 // Pre-cache accounts, host auth's, policies, and special mailboxes 875 preCacheTable(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null); 876 preCacheTable(HostAuth.CONTENT_URI, HostAuth.CONTENT_PROJECTION, null); 877 preCacheTable(Policy.CONTENT_URI, Policy.CONTENT_PROJECTION, null); 878 preCacheTable(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, MAILBOX_PRE_CACHE_SELECTION); 879 880 // Create a map from account,type to a mailbox 881 Map<String, Cursor> snapshot = mCacheMailbox.getSnapshot(); 882 Collection<Cursor> values = snapshot.values(); 883 if (values != null) { 884 for (Cursor c: values) { 885 addToMailboxTypeMap(c); 886 } 887 } 888 } 889 890 /*package*/ static SQLiteDatabase getReadableDatabase(Context context) { 891 DatabaseHelper helper = new DatabaseHelper(context, DATABASE_NAME); 892 return helper.getReadableDatabase(); 893 } 894 895 /** 896 * Restore user Account and HostAuth data from our backup database 897 */ 898 public static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) { 899 if (Email.DEBUG) { 900 Log.w(TAG, "restoreIfNeeded..."); 901 } 902 // Check for legacy backup 903 String legacyBackup = Preferences.getLegacyBackupPreference(context); 904 // If there's a legacy backup, create a new-style backup and delete the legacy backup 905 // In the 1:1000000000 chance that the user gets an app update just as his database becomes 906 // corrupt, oh well... 907 if (!TextUtils.isEmpty(legacyBackup)) { 908 backupAccounts(context, mainDatabase); 909 Preferences.clearLegacyBackupPreference(context); 910 Log.w(TAG, "Created new EmailProvider backup database"); 911 return; 912 } 913 914 // If we have accounts, we're done 915 Cursor c = mainDatabase.query(Account.TABLE_NAME, EmailContent.ID_PROJECTION, null, null, 916 null, null, null); 917 if (c.moveToFirst()) { 918 if (Email.DEBUG) { 919 Log.w(TAG, "restoreIfNeeded: Account exists."); 920 } 921 return; // At least one account exists. 922 } 923 restoreAccounts(context, mainDatabase); 924 } 925 926 /** {@inheritDoc} */ 927 @Override 928 public void shutdown() { 929 if (mDatabase != null) { 930 mDatabase.close(); 931 mDatabase = null; 932 } 933 if (mBodyDatabase != null) { 934 mBodyDatabase.close(); 935 mBodyDatabase = null; 936 } 937 } 938 939 /*package*/ static void deleteOrphans(SQLiteDatabase database, String tableName) { 940 if (database != null) { 941 // We'll look at all of the items in the table; there won't be many typically 942 Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); 943 // Usually, there will be nothing in these tables, so make a quick check 944 try { 945 if (c.getCount() == 0) return; 946 ArrayList<Long> foundMailboxes = new ArrayList<Long>(); 947 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>(); 948 ArrayList<Long> deleteList = new ArrayList<Long>(); 949 String[] bindArray = new String[1]; 950 while (c.moveToNext()) { 951 // Get the mailbox key and see if we've already found this mailbox 952 // If so, we're fine 953 long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY); 954 // If we already know this mailbox doesn't exist, mark the message for deletion 955 if (notFoundMailboxes.contains(mailboxId)) { 956 deleteList.add(c.getLong(ORPHANS_ID)); 957 // If we don't know about this mailbox, we'll try to find it 958 } else if (!foundMailboxes.contains(mailboxId)) { 959 bindArray[0] = Long.toString(mailboxId); 960 Cursor boxCursor = database.query(Mailbox.TABLE_NAME, 961 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null); 962 try { 963 // If it exists, we'll add it to the "found" mailboxes 964 if (boxCursor.moveToFirst()) { 965 foundMailboxes.add(mailboxId); 966 // Otherwise, we'll add to "not found" and mark the message for deletion 967 } else { 968 notFoundMailboxes.add(mailboxId); 969 deleteList.add(c.getLong(ORPHANS_ID)); 970 } 971 } finally { 972 boxCursor.close(); 973 } 974 } 975 } 976 // Now, delete the orphan messages 977 for (long messageId: deleteList) { 978 bindArray[0] = Long.toString(messageId); 979 database.delete(tableName, WHERE_ID, bindArray); 980 } 981 } finally { 982 c.close(); 983 } 984 } 985 } 986 987 private class BodyDatabaseHelper extends SQLiteOpenHelper { 988 BodyDatabaseHelper(Context context, String name) { 989 super(context, name, null, BODY_DATABASE_VERSION); 990 } 991 992 @Override 993 public void onCreate(SQLiteDatabase db) { 994 Log.d(TAG, "Creating EmailProviderBody database"); 995 createBodyTable(db); 996 } 997 998 @Override 999 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 1000 upgradeBodyTable(db, oldVersion, newVersion); 1001 } 1002 1003 @Override 1004 public void onOpen(SQLiteDatabase db) { 1005 } 1006 } 1007 1008 private static class DatabaseHelper extends SQLiteOpenHelper { 1009 Context mContext; 1010 1011 DatabaseHelper(Context context, String name) { 1012 super(context, name, null, DATABASE_VERSION); 1013 mContext = context; 1014 } 1015 1016 @Override 1017 public void onCreate(SQLiteDatabase db) { 1018 Log.d(TAG, "Creating EmailProvider database"); 1019 // Create all tables here; each class has its own method 1020 createMessageTable(db); 1021 createAttachmentTable(db); 1022 createMailboxTable(db); 1023 createHostAuthTable(db); 1024 createAccountTable(db); 1025 createPolicyTable(db); 1026 createQuickResponseTable(db); 1027 } 1028 1029 @Override 1030 @SuppressWarnings("deprecation") 1031 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 1032 // For versions prior to 5, delete all data 1033 // Versions >= 5 require that data be preserved! 1034 if (oldVersion < 5) { 1035 android.accounts.Account[] accounts = AccountManager.get(mContext) 1036 .getAccountsByType(AccountManagerTypes.TYPE_EXCHANGE); 1037 for (android.accounts.Account account: accounts) { 1038 AccountManager.get(mContext).removeAccount(account, null, null); 1039 } 1040 resetMessageTable(db, oldVersion, newVersion); 1041 resetAttachmentTable(db, oldVersion, newVersion); 1042 resetMailboxTable(db, oldVersion, newVersion); 1043 resetHostAuthTable(db, oldVersion, newVersion); 1044 resetAccountTable(db, oldVersion, newVersion); 1045 return; 1046 } 1047 if (oldVersion == 5) { 1048 // Message Tables: Add SyncColumns.SERVER_TIMESTAMP 1049 try { 1050 db.execSQL("alter table " + Message.TABLE_NAME 1051 + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";"); 1052 db.execSQL("alter table " + Message.UPDATED_TABLE_NAME 1053 + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";"); 1054 db.execSQL("alter table " + Message.DELETED_TABLE_NAME 1055 + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";"); 1056 } catch (SQLException e) { 1057 // Shouldn't be needed unless we're debugging and interrupt the process 1058 Log.w(TAG, "Exception upgrading EmailProvider.db from v5 to v6", e); 1059 } 1060 oldVersion = 6; 1061 } 1062 if (oldVersion == 6) { 1063 // Use the newer mailbox_delete trigger 1064 db.execSQL("drop trigger mailbox_delete;"); 1065 db.execSQL(TRIGGER_MAILBOX_DELETE); 1066 oldVersion = 7; 1067 } 1068 if (oldVersion == 7) { 1069 // add the security (provisioning) column 1070 try { 1071 db.execSQL("alter table " + Account.TABLE_NAME 1072 + " add column " + AccountColumns.SECURITY_FLAGS + " integer" + ";"); 1073 } catch (SQLException e) { 1074 // Shouldn't be needed unless we're debugging and interrupt the process 1075 Log.w(TAG, "Exception upgrading EmailProvider.db from 7 to 8 " + e); 1076 } 1077 oldVersion = 8; 1078 } 1079 if (oldVersion == 8) { 1080 // accounts: add security sync key & user signature columns 1081 try { 1082 db.execSQL("alter table " + Account.TABLE_NAME 1083 + " add column " + AccountColumns.SECURITY_SYNC_KEY + " text" + ";"); 1084 db.execSQL("alter table " + Account.TABLE_NAME 1085 + " add column " + AccountColumns.SIGNATURE + " text" + ";"); 1086 } catch (SQLException e) { 1087 // Shouldn't be needed unless we're debugging and interrupt the process 1088 Log.w(TAG, "Exception upgrading EmailProvider.db from 8 to 9 " + e); 1089 } 1090 oldVersion = 9; 1091 } 1092 if (oldVersion == 9) { 1093 // Message: add meeting info column into Message tables 1094 try { 1095 db.execSQL("alter table " + Message.TABLE_NAME 1096 + " add column " + MessageColumns.MEETING_INFO + " text" + ";"); 1097 db.execSQL("alter table " + Message.UPDATED_TABLE_NAME 1098 + " add column " + MessageColumns.MEETING_INFO + " text" + ";"); 1099 db.execSQL("alter table " + Message.DELETED_TABLE_NAME 1100 + " add column " + MessageColumns.MEETING_INFO + " text" + ";"); 1101 } catch (SQLException e) { 1102 // Shouldn't be needed unless we're debugging and interrupt the process 1103 Log.w(TAG, "Exception upgrading EmailProvider.db from 9 to 10 " + e); 1104 } 1105 oldVersion = 10; 1106 } 1107 if (oldVersion == 10) { 1108 // Attachment: add content and flags columns 1109 try { 1110 db.execSQL("alter table " + Attachment.TABLE_NAME 1111 + " add column " + AttachmentColumns.CONTENT + " text" + ";"); 1112 db.execSQL("alter table " + Attachment.TABLE_NAME 1113 + " add column " + AttachmentColumns.FLAGS + " integer" + ";"); 1114 } catch (SQLException e) { 1115 // Shouldn't be needed unless we're debugging and interrupt the process 1116 Log.w(TAG, "Exception upgrading EmailProvider.db from 10 to 11 " + e); 1117 } 1118 oldVersion = 11; 1119 } 1120 if (oldVersion == 11) { 1121 // Attachment: add content_bytes 1122 try { 1123 db.execSQL("alter table " + Attachment.TABLE_NAME 1124 + " add column " + AttachmentColumns.CONTENT_BYTES + " blob" + ";"); 1125 } catch (SQLException e) { 1126 // Shouldn't be needed unless we're debugging and interrupt the process 1127 Log.w(TAG, "Exception upgrading EmailProvider.db from 11 to 12 " + e); 1128 } 1129 oldVersion = 12; 1130 } 1131 if (oldVersion == 12) { 1132 try { 1133 db.execSQL("alter table " + Mailbox.TABLE_NAME 1134 + " add column " + Mailbox.MESSAGE_COUNT 1135 +" integer not null default 0" + ";"); 1136 recalculateMessageCount(db); 1137 } catch (SQLException e) { 1138 // Shouldn't be needed unless we're debugging and interrupt the process 1139 Log.w(TAG, "Exception upgrading EmailProvider.db from 12 to 13 " + e); 1140 } 1141 oldVersion = 13; 1142 } 1143 if (oldVersion == 13) { 1144 try { 1145 db.execSQL("alter table " + Message.TABLE_NAME 1146 + " add column " + Message.SNIPPET 1147 +" text" + ";"); 1148 } catch (SQLException e) { 1149 // Shouldn't be needed unless we're debugging and interrupt the process 1150 Log.w(TAG, "Exception upgrading EmailProvider.db from 13 to 14 " + e); 1151 } 1152 oldVersion = 14; 1153 } 1154 if (oldVersion == 14) { 1155 try { 1156 db.execSQL("alter table " + Message.DELETED_TABLE_NAME 1157 + " add column " + Message.SNIPPET +" text" + ";"); 1158 db.execSQL("alter table " + Message.UPDATED_TABLE_NAME 1159 + " add column " + Message.SNIPPET +" text" + ";"); 1160 } catch (SQLException e) { 1161 // Shouldn't be needed unless we're debugging and interrupt the process 1162 Log.w(TAG, "Exception upgrading EmailProvider.db from 14 to 15 " + e); 1163 } 1164 oldVersion = 15; 1165 } 1166 if (oldVersion == 15) { 1167 try { 1168 db.execSQL("alter table " + Attachment.TABLE_NAME 1169 + " add column " + Attachment.ACCOUNT_KEY +" integer" + ";"); 1170 // Update all existing attachments to add the accountKey data 1171 db.execSQL("update " + Attachment.TABLE_NAME + " set " + 1172 Attachment.ACCOUNT_KEY + "= (SELECT " + Message.TABLE_NAME + "." + 1173 Message.ACCOUNT_KEY + " from " + Message.TABLE_NAME + " where " + 1174 Message.TABLE_NAME + "." + Message.RECORD_ID + " = " + 1175 Attachment.TABLE_NAME + "." + Attachment.MESSAGE_KEY + ")"); 1176 } catch (SQLException e) { 1177 // Shouldn't be needed unless we're debugging and interrupt the process 1178 Log.w(TAG, "Exception upgrading EmailProvider.db from 15 to 16 " + e); 1179 } 1180 oldVersion = 16; 1181 } 1182 if (oldVersion == 16) { 1183 try { 1184 db.execSQL("alter table " + Mailbox.TABLE_NAME 1185 + " add column " + Mailbox.PARENT_KEY + " integer;"); 1186 } catch (SQLException e) { 1187 // Shouldn't be needed unless we're debugging and interrupt the process 1188 Log.w(TAG, "Exception upgrading EmailProvider.db from 16 to 17 " + e); 1189 } 1190 oldVersion = 17; 1191 } 1192 if (oldVersion == 17) { 1193 upgradeFromVersion17ToVersion18(db); 1194 oldVersion = 18; 1195 } 1196 if (oldVersion == 18) { 1197 try { 1198 db.execSQL("alter table " + Account.TABLE_NAME 1199 + " add column " + Account.POLICY_KEY + " integer;"); 1200 db.execSQL("drop trigger account_delete;"); 1201 db.execSQL(TRIGGER_ACCOUNT_DELETE); 1202 createPolicyTable(db); 1203 convertPolicyFlagsToPolicyTable(db); 1204 } catch (SQLException e) { 1205 // Shouldn't be needed unless we're debugging and interrupt the process 1206 Log.w(TAG, "Exception upgrading EmailProvider.db from 18 to 19 " + e); 1207 } 1208 oldVersion = 19; 1209 } 1210 if (oldVersion == 19) { 1211 try { 1212 db.execSQL("alter table " + Policy.TABLE_NAME 1213 + " add column " + PolicyColumns.REQUIRE_MANUAL_SYNC_WHEN_ROAMING + 1214 " integer;"); 1215 db.execSQL("alter table " + Policy.TABLE_NAME 1216 + " add column " + PolicyColumns.DONT_ALLOW_CAMERA + " integer;"); 1217 db.execSQL("alter table " + Policy.TABLE_NAME 1218 + " add column " + PolicyColumns.DONT_ALLOW_ATTACHMENTS + " integer;"); 1219 db.execSQL("alter table " + Policy.TABLE_NAME 1220 + " add column " + PolicyColumns.DONT_ALLOW_HTML + " integer;"); 1221 db.execSQL("alter table " + Policy.TABLE_NAME 1222 + " add column " + PolicyColumns.MAX_ATTACHMENT_SIZE + " integer;"); 1223 db.execSQL("alter table " + Policy.TABLE_NAME 1224 + " add column " + PolicyColumns.MAX_TEXT_TRUNCATION_SIZE + 1225 " integer;"); 1226 db.execSQL("alter table " + Policy.TABLE_NAME 1227 + " add column " + PolicyColumns.MAX_HTML_TRUNCATION_SIZE + 1228 " integer;"); 1229 db.execSQL("alter table " + Policy.TABLE_NAME 1230 + " add column " + PolicyColumns.MAX_EMAIL_LOOKBACK + " integer;"); 1231 db.execSQL("alter table " + Policy.TABLE_NAME 1232 + " add column " + PolicyColumns.MAX_CALENDAR_LOOKBACK + " integer;"); 1233 db.execSQL("alter table " + Policy.TABLE_NAME 1234 + " add column " + PolicyColumns.PASSWORD_RECOVERY_ENABLED + 1235 " integer;"); 1236 } catch (SQLException e) { 1237 // Shouldn't be needed unless we're debugging and interrupt the process 1238 Log.w(TAG, "Exception upgrading EmailProvider.db from 19 to 20 " + e); 1239 } 1240 oldVersion = 20; 1241 } 1242 if (oldVersion == 20) { 1243 upgradeFromVersion20ToVersion21(db); 1244 oldVersion = 21; 1245 } 1246 if (oldVersion == 21) { 1247 upgradeFromVersion21ToVersion22(db, mContext); 1248 oldVersion = 22; 1249 } 1250 if (oldVersion == 22) { 1251 upgradeFromVersion22ToVersion23(db); 1252 oldVersion = 23; 1253 } 1254 if (oldVersion == 23) { 1255 upgradeFromVersion23ToVersion24(db); 1256 oldVersion = 24; 1257 } 1258 if (oldVersion == 24) { 1259 upgradeFromVersion24ToVersion25(db); 1260 oldVersion = 25; 1261 } 1262 if (oldVersion == 25) { 1263 upgradeFromVersion25ToVersion26(db); 1264 oldVersion = 26; 1265 } 1266 if (oldVersion == 26) { 1267 try { 1268 db.execSQL("alter table " + Message.TABLE_NAME 1269 + " add column " + Message.PROTOCOL_SEARCH_INFO + " text;"); 1270 db.execSQL("alter table " + Message.DELETED_TABLE_NAME 1271 + " add column " + Message.PROTOCOL_SEARCH_INFO +" text" + ";"); 1272 db.execSQL("alter table " + Message.UPDATED_TABLE_NAME 1273 + " add column " + Message.PROTOCOL_SEARCH_INFO +" text" + ";"); 1274 } catch (SQLException e) { 1275 // Shouldn't be needed unless we're debugging and interrupt the process 1276 Log.w(TAG, "Exception upgrading EmailProvider.db from 26 to 27 " + e); 1277 } 1278 oldVersion = 27; 1279 } 1280 } 1281 1282 @Override 1283 public void onOpen(SQLiteDatabase db) { 1284 } 1285 } 1286 1287 @Override 1288 public int delete(Uri uri, String selection, String[] selectionArgs) { 1289 final int match = findMatch(uri, "delete"); 1290 Context context = getContext(); 1291 // Pick the correct database for this operation 1292 // If we're in a transaction already (which would happen during applyBatch), then the 1293 // body database is already attached to the email database and any attempt to use the 1294 // body database directly will result in a SQLiteException (the database is locked) 1295 SQLiteDatabase db = getDatabase(context); 1296 int table = match >> BASE_SHIFT; 1297 String id = "0"; 1298 boolean messageDeletion = false; 1299 ContentResolver resolver = context.getContentResolver(); 1300 1301 ContentCache cache = mContentCaches[table]; 1302 String tableName = TABLE_NAMES[table]; 1303 int result = -1; 1304 1305 try { 1306 switch (match) { 1307 // These are cases in which one or more Messages might get deleted, either by 1308 // cascade or explicitly 1309 case MAILBOX_ID: 1310 case MAILBOX: 1311 case ACCOUNT_ID: 1312 case ACCOUNT: 1313 case MESSAGE: 1314 case SYNCED_MESSAGE_ID: 1315 case MESSAGE_ID: 1316 // Handle lost Body records here, since this cannot be done in a trigger 1317 // The process is: 1318 // 1) Begin a transaction, ensuring that both databases are affected atomically 1319 // 2) Do the requested deletion, with cascading deletions handled in triggers 1320 // 3) End the transaction, committing all changes atomically 1321 // 1322 // Bodies are auto-deleted here; Attachments are auto-deleted via trigger 1323 messageDeletion = true; 1324 db.beginTransaction(); 1325 break; 1326 } 1327 switch (match) { 1328 case BODY_ID: 1329 case DELETED_MESSAGE_ID: 1330 case SYNCED_MESSAGE_ID: 1331 case MESSAGE_ID: 1332 case UPDATED_MESSAGE_ID: 1333 case ATTACHMENT_ID: 1334 case MAILBOX_ID: 1335 case ACCOUNT_ID: 1336 case HOSTAUTH_ID: 1337 case POLICY_ID: 1338 case QUICK_RESPONSE_ID: 1339 id = uri.getPathSegments().get(1); 1340 if (match == SYNCED_MESSAGE_ID) { 1341 // For synced messages, first copy the old message to the deleted table and 1342 // delete it from the updated table (in case it was updated first) 1343 // Note that this is all within a transaction, for atomicity 1344 db.execSQL(DELETED_MESSAGE_INSERT + id); 1345 db.execSQL(UPDATED_MESSAGE_DELETE + id); 1346 } 1347 if (cache != null) { 1348 cache.lock(id); 1349 } 1350 try { 1351 result = db.delete(tableName, whereWithId(id, selection), selectionArgs); 1352 if (cache != null) { 1353 switch(match) { 1354 case ACCOUNT_ID: 1355 // Account deletion will clear all of the caches, as HostAuth's, 1356 // Mailboxes, and Messages will be deleted in the process 1357 mCacheMailbox.invalidate("Delete", uri, selection); 1358 mCacheHostAuth.invalidate("Delete", uri, selection); 1359 mCachePolicy.invalidate("Delete", uri, selection); 1360 //$FALL-THROUGH$ 1361 case MAILBOX_ID: 1362 // Mailbox deletion will clear the Message cache 1363 mCacheMessage.invalidate("Delete", uri, selection); 1364 //$FALL-THROUGH$ 1365 case SYNCED_MESSAGE_ID: 1366 case MESSAGE_ID: 1367 case HOSTAUTH_ID: 1368 case POLICY_ID: 1369 cache.invalidate("Delete", uri, selection); 1370 // Make sure all data is properly cached 1371 if (match != MESSAGE_ID) { 1372 preCacheData(); 1373 } 1374 break; 1375 } 1376 } 1377 } finally { 1378 if (cache != null) { 1379 cache.unlock(id); 1380 } 1381 } 1382 break; 1383 case ATTACHMENTS_MESSAGE_ID: 1384 // All attachments for the given message 1385 id = uri.getPathSegments().get(2); 1386 result = db.delete(tableName, 1387 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs); 1388 break; 1389 1390 case BODY: 1391 case MESSAGE: 1392 case DELETED_MESSAGE: 1393 case UPDATED_MESSAGE: 1394 case ATTACHMENT: 1395 case MAILBOX: 1396 case ACCOUNT: 1397 case HOSTAUTH: 1398 case POLICY: 1399 switch(match) { 1400 // See the comments above for deletion of ACCOUNT_ID, etc 1401 case ACCOUNT: 1402 mCacheMailbox.invalidate("Delete", uri, selection); 1403 mCacheHostAuth.invalidate("Delete", uri, selection); 1404 mCachePolicy.invalidate("Delete", uri, selection); 1405 //$FALL-THROUGH$ 1406 case MAILBOX: 1407 mCacheMessage.invalidate("Delete", uri, selection); 1408 //$FALL-THROUGH$ 1409 case MESSAGE: 1410 case HOSTAUTH: 1411 case POLICY: 1412 cache.invalidate("Delete", uri, selection); 1413 break; 1414 } 1415 result = db.delete(tableName, selection, selectionArgs); 1416 switch(match) { 1417 case ACCOUNT: 1418 case MAILBOX: 1419 case HOSTAUTH: 1420 case POLICY: 1421 // Make sure all data is properly cached 1422 preCacheData(); 1423 break; 1424 } 1425 break; 1426 1427 default: 1428 throw new IllegalArgumentException("Unknown URI " + uri); 1429 } 1430 if (messageDeletion) { 1431 if (match == MESSAGE_ID) { 1432 // Delete the Body record associated with the deleted message 1433 db.execSQL(DELETE_BODY + id); 1434 } else { 1435 // Delete any orphaned Body records 1436 db.execSQL(DELETE_ORPHAN_BODIES); 1437 } 1438 db.setTransactionSuccessful(); 1439 } 1440 } catch (SQLiteException e) { 1441 checkDatabases(); 1442 throw e; 1443 } finally { 1444 if (messageDeletion) { 1445 db.endTransaction(); 1446 } 1447 } 1448 1449 // Notify all notifier cursors 1450 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id); 1451 1452 // Notify all email content cursors 1453 resolver.notifyChange(EmailContent.CONTENT_URI, null); 1454 return result; 1455 } 1456 1457 @Override 1458 // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) 1459 public String getType(Uri uri) { 1460 int match = findMatch(uri, "getType"); 1461 switch (match) { 1462 case BODY_ID: 1463 return "vnd.android.cursor.item/email-body"; 1464 case BODY: 1465 return "vnd.android.cursor.dir/email-body"; 1466 case UPDATED_MESSAGE_ID: 1467 case MESSAGE_ID: 1468 // NOTE: According to the framework folks, we're supposed to invent mime types as 1469 // a way of passing information to drag & drop recipients. 1470 // If there's a mailboxId parameter in the url, we respond with a mime type that 1471 // has -n appended, where n is the mailboxId of the message. The drag & drop code 1472 // uses this information to know not to allow dragging the item to its own mailbox 1473 String mimeType = EMAIL_MESSAGE_MIME_TYPE; 1474 String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID); 1475 if (mailboxId != null) { 1476 mimeType += "-" + mailboxId; 1477 } 1478 return mimeType; 1479 case UPDATED_MESSAGE: 1480 case MESSAGE: 1481 return "vnd.android.cursor.dir/email-message"; 1482 case MAILBOX: 1483 return "vnd.android.cursor.dir/email-mailbox"; 1484 case MAILBOX_ID: 1485 return "vnd.android.cursor.item/email-mailbox"; 1486 case ACCOUNT: 1487 return "vnd.android.cursor.dir/email-account"; 1488 case ACCOUNT_ID: 1489 return "vnd.android.cursor.item/email-account"; 1490 case ATTACHMENTS_MESSAGE_ID: 1491 case ATTACHMENT: 1492 return "vnd.android.cursor.dir/email-attachment"; 1493 case ATTACHMENT_ID: 1494 return EMAIL_ATTACHMENT_MIME_TYPE; 1495 case HOSTAUTH: 1496 return "vnd.android.cursor.dir/email-hostauth"; 1497 case HOSTAUTH_ID: 1498 return "vnd.android.cursor.item/email-hostauth"; 1499 default: 1500 throw new IllegalArgumentException("Unknown URI " + uri); 1501 } 1502 } 1503 1504 @Override 1505 public Uri insert(Uri uri, ContentValues values) { 1506 int match = findMatch(uri, "insert"); 1507 Context context = getContext(); 1508 ContentResolver resolver = context.getContentResolver(); 1509 1510 // See the comment at delete(), above 1511 SQLiteDatabase db = getDatabase(context); 1512 int table = match >> BASE_SHIFT; 1513 String id = "0"; 1514 long longId; 1515 1516 // We do NOT allow setting of unreadCount/messageCount via the provider 1517 // These columns are maintained via triggers 1518 if (match == MAILBOX_ID || match == MAILBOX) { 1519 values.put(MailboxColumns.UNREAD_COUNT, 0); 1520 values.put(MailboxColumns.MESSAGE_COUNT, 0); 1521 } 1522 1523 Uri resultUri = null; 1524 1525 try { 1526 switch (match) { 1527 // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE 1528 // or DELETED_MESSAGE; see the comment below for details 1529 case UPDATED_MESSAGE: 1530 case DELETED_MESSAGE: 1531 case MESSAGE: 1532 case BODY: 1533 case ATTACHMENT: 1534 case MAILBOX: 1535 case ACCOUNT: 1536 case HOSTAUTH: 1537 case POLICY: 1538 case QUICK_RESPONSE: 1539 longId = db.insert(TABLE_NAMES[table], "foo", values); 1540 resultUri = ContentUris.withAppendedId(uri, longId); 1541 switch(match) { 1542 case MAILBOX: 1543 if (values.containsKey(MailboxColumns.TYPE)) { 1544 // Only cache special mailbox types 1545 int type = values.getAsInteger(MailboxColumns.TYPE); 1546 if (type != Mailbox.TYPE_INBOX && type != Mailbox.TYPE_OUTBOX && 1547 type != Mailbox.TYPE_DRAFTS && type != Mailbox.TYPE_SENT && 1548 type != Mailbox.TYPE_TRASH && type != Mailbox.TYPE_SEARCH) { 1549 break; 1550 } 1551 } 1552 //$FALL-THROUGH$ 1553 case ACCOUNT: 1554 case HOSTAUTH: 1555 case POLICY: 1556 // Cache new account, host auth, policy, and some mailbox rows 1557 Cursor c = query(resultUri, CACHE_PROJECTIONS[table], null, null, null); 1558 if (c != null) { 1559 if (match == MAILBOX) { 1560 addToMailboxTypeMap(c); 1561 } else if (match == ACCOUNT) { 1562 getOrCreateAccountMailboxTypeMap(longId); 1563 } 1564 c.close(); 1565 } 1566 break; 1567 } 1568 // Clients shouldn't normally be adding rows to these tables, as they are 1569 // maintained by triggers. However, we need to be able to do this for unit 1570 // testing, so we allow the insert and then throw the same exception that we 1571 // would if this weren't allowed. 1572 if (match == UPDATED_MESSAGE || match == DELETED_MESSAGE) { 1573 throw new IllegalArgumentException("Unknown URL " + uri); 1574 } 1575 if (match == ATTACHMENT) { 1576 int flags = 0; 1577 if (values.containsKey(Attachment.FLAGS)) { 1578 flags = values.getAsInteger(Attachment.FLAGS); 1579 } 1580 // Report all new attachments to the download service 1581 AttachmentDownloadService.attachmentChanged(longId, flags); 1582 } 1583 break; 1584 case MAILBOX_ID: 1585 // This implies adding a message to a mailbox 1586 // Hmm, a problem here is that we can't link the account as well, so it must be 1587 // already in the values... 1588 longId = Long.parseLong(uri.getPathSegments().get(1)); 1589 values.put(MessageColumns.MAILBOX_KEY, longId); 1590 return insert(Message.CONTENT_URI, values); // Recurse 1591 case MESSAGE_ID: 1592 // This implies adding an attachment to a message. 1593 id = uri.getPathSegments().get(1); 1594 longId = Long.parseLong(id); 1595 values.put(AttachmentColumns.MESSAGE_KEY, longId); 1596 return insert(Attachment.CONTENT_URI, values); // Recurse 1597 case ACCOUNT_ID: 1598 // This implies adding a mailbox to an account. 1599 longId = Long.parseLong(uri.getPathSegments().get(1)); 1600 values.put(MailboxColumns.ACCOUNT_KEY, longId); 1601 return insert(Mailbox.CONTENT_URI, values); // Recurse 1602 case ATTACHMENTS_MESSAGE_ID: 1603 longId = db.insert(TABLE_NAMES[table], "foo", values); 1604 resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId); 1605 break; 1606 default: 1607 throw new IllegalArgumentException("Unknown URL " + uri); 1608 } 1609 } catch (SQLiteException e) { 1610 checkDatabases(); 1611 throw e; 1612 } 1613 1614 // Notify all notifier cursors 1615 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id); 1616 1617 // Notify all existing cursors. 1618 resolver.notifyChange(EmailContent.CONTENT_URI, null); 1619 return resultUri; 1620 } 1621 1622 @Override 1623 public boolean onCreate() { 1624 checkDatabases(); 1625 return false; 1626 } 1627 1628 /** 1629 * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must 1630 * always be in sync (i.e. there are two database or NO databases). This code will delete 1631 * any "orphan" database, so that both will be created together. Note that an "orphan" database 1632 * will exist after either of the individual databases is deleted due to data corruption. 1633 */ 1634 public void checkDatabases() { 1635 // Uncache the databases 1636 if (mDatabase != null) { 1637 mDatabase = null; 1638 } 1639 if (mBodyDatabase != null) { 1640 mBodyDatabase = null; 1641 } 1642 // Look for orphans, and delete as necessary; these must always be in sync 1643 File databaseFile = getContext().getDatabasePath(DATABASE_NAME); 1644 File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME); 1645 1646 // TODO Make sure attachments are deleted 1647 if (databaseFile.exists() && !bodyFile.exists()) { 1648 Log.w(TAG, "Deleting orphaned EmailProvider database..."); 1649 databaseFile.delete(); 1650 } else if (bodyFile.exists() && !databaseFile.exists()) { 1651 Log.w(TAG, "Deleting orphaned EmailProviderBody database..."); 1652 bodyFile.delete(); 1653 } 1654 } 1655 1656 @Override 1657 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1658 String sortOrder) { 1659 long time = 0L; 1660 if (Email.DEBUG) { 1661 time = System.nanoTime(); 1662 } 1663 Cursor c = null; 1664 int match; 1665 try { 1666 match = findMatch(uri, "query"); 1667 } catch (IllegalArgumentException e) { 1668 String uriString = uri.toString(); 1669 // If we were passed an illegal uri, see if it ends in /-1 1670 // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor 1671 if (uriString != null && uriString.endsWith("/-1")) { 1672 uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0"); 1673 match = findMatch(uri, "query"); 1674 switch (match) { 1675 case BODY_ID: 1676 case MESSAGE_ID: 1677 case DELETED_MESSAGE_ID: 1678 case UPDATED_MESSAGE_ID: 1679 case ATTACHMENT_ID: 1680 case MAILBOX_ID: 1681 case ACCOUNT_ID: 1682 case HOSTAUTH_ID: 1683 case POLICY_ID: 1684 return new MatrixCursor(projection, 0); 1685 } 1686 } 1687 throw e; 1688 } 1689 Context context = getContext(); 1690 // See the comment at delete(), above 1691 SQLiteDatabase db = getDatabase(context); 1692 int table = match >> BASE_SHIFT; 1693 String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT); 1694 String id; 1695 1696 // Find the cache for this query's table (if any) 1697 ContentCache cache = null; 1698 String tableName = TABLE_NAMES[table]; 1699 // We can only use the cache if there's no selection 1700 if (selection == null) { 1701 cache = mContentCaches[table]; 1702 } 1703 if (cache == null) { 1704 ContentCache.notCacheable(uri, selection); 1705 } 1706 1707 try { 1708 switch (match) { 1709 case ACCOUNT_DEFAULT_ID: 1710 // Start with a snapshot of the cache 1711 Map<String, Cursor> accountCache = mCacheAccount.getSnapshot(); 1712 long accountId = Account.NO_ACCOUNT; 1713 // Find the account with "isDefault" set 1714 Collection<Cursor> accounts = accountCache.values(); 1715 for (Cursor accountCursor: accounts) { 1716 // For now, at least, we can have zero count cursors (e.g. if someone looks 1717 // up a non-existent id); we need to skip these 1718 if (accountCursor.moveToFirst()) { 1719 boolean isDefault = 1720 accountCursor.getInt(Account.CONTENT_IS_DEFAULT_COLUMN) == 1; 1721 // We'll remember this one if it's the default or the first one we see 1722 if (isDefault || accountId == Account.NO_ACCOUNT) { 1723 accountId = accountCursor.getLong(Account.CONTENT_ID_COLUMN); 1724 // If it's the default, we're done 1725 if (isDefault) break; 1726 } 1727 } 1728 } 1729 // Return a cursor with an id projection 1730 MatrixCursor mc = new MatrixCursor(EmailContent.ID_PROJECTION); 1731 mc.addRow(new Object[] {accountId}); 1732 return mc; 1733 case MAILBOX_ID_FROM_ACCOUNT_AND_TYPE: 1734 // Get accountId and type and find the mailbox in our map 1735 accountId = Long.parseLong(uri.getPathSegments().get(1)); 1736 int type = Integer.parseInt(uri.getPathSegments().get(2)); 1737 HashMap<Integer, Long> accountMap = mMailboxTypeMap.get(accountId); 1738 mc = new MatrixCursor(EmailContent.ID_PROJECTION); 1739 Long mailboxId = null; 1740 if (accountMap != null) { 1741 mailboxId = accountMap.get(type); 1742 } 1743 // Return a cursor with an id projection 1744 mc.addRow(new Object[] {mailboxId}); 1745 return mc; 1746 case BODY: 1747 case MESSAGE: 1748 case UPDATED_MESSAGE: 1749 case DELETED_MESSAGE: 1750 case ATTACHMENT: 1751 case MAILBOX: 1752 case ACCOUNT: 1753 case HOSTAUTH: 1754 case POLICY: 1755 case QUICK_RESPONSE: 1756 // Special-case "count of accounts"; it's common and we always know it 1757 if (match == ACCOUNT && Arrays.equals(projection, EmailContent.COUNT_COLUMNS) && 1758 selection == null && limit.equals("1")) { 1759 int accountCount = mMailboxTypeMap.size(); 1760 // In the rare case there are MAX_CACHED_ACCOUNTS or more, we can't do this 1761 if (accountCount < MAX_CACHED_ACCOUNTS) { 1762 mc = new MatrixCursor(projection, 1); 1763 mc.addRow(new Object[] {accountCount}); 1764 return mc; 1765 } 1766 } 1767 c = db.query(tableName, projection, 1768 selection, selectionArgs, null, null, sortOrder, limit); 1769 break; 1770 case BODY_ID: 1771 case MESSAGE_ID: 1772 case DELETED_MESSAGE_ID: 1773 case UPDATED_MESSAGE_ID: 1774 case ATTACHMENT_ID: 1775 case MAILBOX_ID: 1776 case ACCOUNT_ID: 1777 case HOSTAUTH_ID: 1778 case POLICY_ID: 1779 case QUICK_RESPONSE_ID: 1780 id = uri.getPathSegments().get(1); 1781 if (cache != null) { 1782 c = cache.getCachedCursor(id, projection); 1783 } 1784 if (c == null) { 1785 CacheToken token = null; 1786 if (cache != null) { 1787 token = cache.getCacheToken(id); 1788 } 1789 c = db.query(tableName, projection, whereWithId(id, selection), 1790 selectionArgs, null, null, sortOrder, limit); 1791 if (Email.DEBUG) { 1792 switch(match) { 1793 case ACCOUNT_ID: 1794 case HOSTAUTH_ID: 1795 case POLICY_ID: 1796 case MAILBOX_ID: 1797 Log.w(Logging.LOG_TAG, 1798 "==== UNCACHED read of " + tableName + ", id = " + id); 1799 break; 1800 } 1801 } 1802 if (cache != null) { 1803 c = cache.putCursor(c, id, projection, token); 1804 } 1805 } 1806 break; 1807 case ATTACHMENTS_MESSAGE_ID: 1808 // All attachments for the given message 1809 id = uri.getPathSegments().get(2); 1810 c = db.query(Attachment.TABLE_NAME, projection, 1811 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), 1812 selectionArgs, null, null, sortOrder, limit); 1813 break; 1814 case QUICK_RESPONSE_ACCOUNT_ID: 1815 // All quick responses for the given account 1816 id = uri.getPathSegments().get(2); 1817 c = db.query(QuickResponse.TABLE_NAME, projection, 1818 whereWith(QuickResponse.ACCOUNT_KEY + "=" + id, selection), 1819 selectionArgs, null, null, sortOrder); 1820 break; 1821 default: 1822 throw new IllegalArgumentException("Unknown URI " + uri); 1823 } 1824 } catch (SQLiteException e) { 1825 checkDatabases(); 1826 throw e; 1827 } catch (RuntimeException e) { 1828 checkDatabases(); 1829 e.printStackTrace(); 1830 throw e; 1831 } finally { 1832 if (cache != null && c != null && Email.DEBUG) { 1833 cache.recordQueryTime(c, System.nanoTime() - time); 1834 } 1835 } 1836 1837 if ((c != null) && !isTemporary()) { 1838 c.setNotificationUri(getContext().getContentResolver(), uri); 1839 } 1840 return c; 1841 } 1842 1843 private String whereWithId(String id, String selection) { 1844 StringBuilder sb = new StringBuilder(256); 1845 sb.append("_id="); 1846 sb.append(id); 1847 if (selection != null) { 1848 sb.append(" AND ("); 1849 sb.append(selection); 1850 sb.append(')'); 1851 } 1852 return sb.toString(); 1853 } 1854 1855 /** 1856 * Combine a locally-generated selection with a user-provided selection 1857 * 1858 * This introduces risk that the local selection might insert incorrect chars 1859 * into the SQL, so use caution. 1860 * 1861 * @param where locally-generated selection, must not be null 1862 * @param selection user-provided selection, may be null 1863 * @return a single selection string 1864 */ 1865 private String whereWith(String where, String selection) { 1866 if (selection == null) { 1867 return where; 1868 } 1869 StringBuilder sb = new StringBuilder(where); 1870 sb.append(" AND ("); 1871 sb.append(selection); 1872 sb.append(')'); 1873 1874 return sb.toString(); 1875 } 1876 1877 /** 1878 * Restore a HostAuth from a database, given its unique id 1879 * @param db the database 1880 * @param id the unique id (_id) of the row 1881 * @return a fully populated HostAuth or null if the row does not exist 1882 */ 1883 private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) { 1884 Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION, 1885 HostAuth.RECORD_ID + "=?", new String[] {Long.toString(id)}, null, null, null); 1886 try { 1887 if (c.moveToFirst()) { 1888 HostAuth hostAuth = new HostAuth(); 1889 hostAuth.restore(c); 1890 return hostAuth; 1891 } 1892 return null; 1893 } finally { 1894 c.close(); 1895 } 1896 } 1897 1898 /** 1899 * Copy the Account and HostAuth tables from one database to another 1900 * @param fromDatabase the source database 1901 * @param toDatabase the destination database 1902 * @return the number of accounts copied, or -1 if an error occurred 1903 */ 1904 private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) { 1905 if (fromDatabase == null || toDatabase == null) return -1; 1906 int copyCount = 0; 1907 try { 1908 // Lock both databases; for the "from" database, we don't want anyone changing it from 1909 // under us; for the "to" database, we want to make the operation atomic 1910 fromDatabase.beginTransaction(); 1911 toDatabase.beginTransaction(); 1912 // Delete anything hanging around here 1913 toDatabase.delete(Account.TABLE_NAME, null, null); 1914 toDatabase.delete(HostAuth.TABLE_NAME, null, null); 1915 // Get our account cursor 1916 Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION, 1917 null, null, null, null, null); 1918 boolean noErrors = true; 1919 try { 1920 // Loop through accounts, copying them and associated host auth's 1921 while (c.moveToNext()) { 1922 Account account = new Account(); 1923 account.restore(c); 1924 1925 // Clear security sync key and sync key, as these were specific to the state of 1926 // the account, and we've reset that... 1927 // Clear policy key so that we can re-establish policies from the server 1928 // TODO This is pretty EAS specific, but there's a lot of that around 1929 account.mSecuritySyncKey = null; 1930 account.mSyncKey = null; 1931 account.mPolicyKey = 0; 1932 1933 // Copy host auth's and update foreign keys 1934 HostAuth hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeyRecv); 1935 // The account might have gone away, though very unlikely 1936 if (hostAuth == null) continue; 1937 account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null, 1938 hostAuth.toContentValues()); 1939 // EAS accounts have no send HostAuth 1940 if (account.mHostAuthKeySend > 0) { 1941 hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend); 1942 // Belt and suspenders; I can't imagine that this is possible, since we 1943 // checked the validity of the account above, and the database is now locked 1944 if (hostAuth == null) continue; 1945 account.mHostAuthKeySend = toDatabase.insert(HostAuth.TABLE_NAME, null, 1946 hostAuth.toContentValues()); 1947 } 1948 // Now, create the account in the "to" database 1949 toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues()); 1950 copyCount++; 1951 } 1952 } catch (SQLiteException e) { 1953 noErrors = false; 1954 copyCount = -1; 1955 } finally { 1956 fromDatabase.endTransaction(); 1957 if (noErrors) { 1958 // Say it's ok to commit 1959 toDatabase.setTransactionSuccessful(); 1960 } 1961 toDatabase.endTransaction(); 1962 c.close(); 1963 } 1964 } catch (SQLiteException e) { 1965 copyCount = -1; 1966 } 1967 return copyCount; 1968 } 1969 1970 private static SQLiteDatabase getBackupDatabase(Context context) { 1971 DatabaseHelper helper = new DatabaseHelper(context, BACKUP_DATABASE_NAME); 1972 return helper.getWritableDatabase(); 1973 } 1974 1975 /** 1976 * Backup account data, returning the number of accounts backed up 1977 */ 1978 private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) { 1979 if (Email.DEBUG) { 1980 Log.d(TAG, "backupAccounts..."); 1981 } 1982 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1983 try { 1984 int numBackedUp = copyAccountTables(mainDatabase, backupDatabase); 1985 if (numBackedUp < 0) { 1986 Log.e(TAG, "Account backup failed!"); 1987 } else if (Email.DEBUG) { 1988 Log.d(TAG, "Backed up " + numBackedUp + " accounts..."); 1989 } 1990 return numBackedUp; 1991 } finally { 1992 if (backupDatabase != null) { 1993 backupDatabase.close(); 1994 } 1995 } 1996 } 1997 1998 /** 1999 * Restore account data, returning the number of accounts restored 2000 */ 2001 private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) { 2002 if (Email.DEBUG) { 2003 Log.d(TAG, "restoreAccounts..."); 2004 } 2005 SQLiteDatabase backupDatabase = getBackupDatabase(context); 2006 try { 2007 int numRecovered = copyAccountTables(backupDatabase, mainDatabase); 2008 if (numRecovered > 0) { 2009 Log.e(TAG, "Recovered " + numRecovered + " accounts!"); 2010 } else if (numRecovered < 0) { 2011 Log.e(TAG, "Account recovery failed?"); 2012 } else if (Email.DEBUG) { 2013 Log.d(TAG, "No accounts to restore..."); 2014 } 2015 return numRecovered; 2016 } finally { 2017 if (backupDatabase != null) { 2018 backupDatabase.close(); 2019 } 2020 } 2021 } 2022 2023 @Override 2024 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 2025 // Handle this special case the fastest possible way 2026 if (uri == INTEGRITY_CHECK_URI) { 2027 checkDatabases(); 2028 return 0; 2029 } else if (uri == ACCOUNT_BACKUP_URI) { 2030 return backupAccounts(getContext(), getDatabase(getContext())); 2031 } 2032 2033 // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID) 2034 Uri notificationUri = EmailContent.CONTENT_URI; 2035 2036 int match = findMatch(uri, "update"); 2037 Context context = getContext(); 2038 ContentResolver resolver = context.getContentResolver(); 2039 // See the comment at delete(), above 2040 SQLiteDatabase db = getDatabase(context); 2041 int table = match >> BASE_SHIFT; 2042 int result; 2043 2044 // We do NOT allow setting of unreadCount/messageCount via the provider 2045 // These columns are maintained via triggers 2046 if (match == MAILBOX_ID || match == MAILBOX) { 2047 values.remove(MailboxColumns.UNREAD_COUNT); 2048 values.remove(MailboxColumns.MESSAGE_COUNT); 2049 } 2050 2051 ContentCache cache = mContentCaches[table]; 2052 String tableName = TABLE_NAMES[table]; 2053 String id = "0"; 2054 2055 try { 2056outer: 2057 switch (match) { 2058 case MAILBOX_ID_ADD_TO_FIELD: 2059 case ACCOUNT_ID_ADD_TO_FIELD: 2060 id = uri.getPathSegments().get(1); 2061 String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME); 2062 Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME); 2063 if (field == null || add == null) { 2064 throw new IllegalArgumentException("No field/add specified " + uri); 2065 } 2066 ContentValues actualValues = new ContentValues(); 2067 if (cache != null) { 2068 cache.lock(id); 2069 } 2070 try { 2071 db.beginTransaction(); 2072 try { 2073 Cursor c = db.query(tableName, 2074 new String[] {EmailContent.RECORD_ID, field}, 2075 whereWithId(id, selection), 2076 selectionArgs, null, null, null); 2077 try { 2078 result = 0; 2079 String[] bind = new String[1]; 2080 if (c.moveToNext()) { 2081 bind[0] = c.getString(0); // _id 2082 long value = c.getLong(1) + add; 2083 actualValues.put(field, value); 2084 result = db.update(tableName, actualValues, ID_EQUALS, bind); 2085 } 2086 db.setTransactionSuccessful(); 2087 } finally { 2088 c.close(); 2089 } 2090 } finally { 2091 db.endTransaction(); 2092 } 2093 } finally { 2094 if (cache != null) { 2095 cache.unlock(id, actualValues); 2096 } 2097 } 2098 break; 2099 case SYNCED_MESSAGE_ID: 2100 case UPDATED_MESSAGE_ID: 2101 case MESSAGE_ID: 2102 case BODY_ID: 2103 case ATTACHMENT_ID: 2104 case MAILBOX_ID: 2105 case ACCOUNT_ID: 2106 case HOSTAUTH_ID: 2107 case QUICK_RESPONSE_ID: 2108 case POLICY_ID: 2109 id = uri.getPathSegments().get(1); 2110 if (cache != null) { 2111 cache.lock(id); 2112 } 2113 try { 2114 if (match == SYNCED_MESSAGE_ID) { 2115 // For synced messages, first copy the old message to the updated table 2116 // Note the insert or ignore semantics, guaranteeing that only the first 2117 // update will be reflected in the updated message table; therefore this 2118 // row will always have the "original" data 2119 db.execSQL(UPDATED_MESSAGE_INSERT + id); 2120 } else if (match == MESSAGE_ID) { 2121 db.execSQL(UPDATED_MESSAGE_DELETE + id); 2122 } 2123 result = db.update(tableName, values, whereWithId(id, selection), 2124 selectionArgs); 2125 } catch (SQLiteException e) { 2126 // Null out values (so they aren't cached) and re-throw 2127 values = null; 2128 throw e; 2129 } finally { 2130 if (cache != null) { 2131 cache.unlock(id, values); 2132 } 2133 } 2134 if (match == ATTACHMENT_ID) { 2135 if (values.containsKey(Attachment.FLAGS)) { 2136 int flags = values.getAsInteger(Attachment.FLAGS); 2137 AttachmentDownloadService.attachmentChanged( 2138 Integer.parseInt(id), flags); 2139 } 2140 } 2141 break; 2142 case BODY: 2143 case MESSAGE: 2144 case UPDATED_MESSAGE: 2145 case ATTACHMENT: 2146 case MAILBOX: 2147 case ACCOUNT: 2148 case HOSTAUTH: 2149 case POLICY: 2150 switch(match) { 2151 // To avoid invalidating the cache on updates, we execute them one at a 2152 // time using the XXX_ID uri; these are all executed atomically 2153 case ACCOUNT: 2154 case MAILBOX: 2155 case HOSTAUTH: 2156 case POLICY: 2157 Cursor c = db.query(tableName, EmailContent.ID_PROJECTION, 2158 selection, selectionArgs, null, null, null); 2159 db.beginTransaction(); 2160 result = 0; 2161 try { 2162 while (c.moveToNext()) { 2163 update(ContentUris.withAppendedId( 2164 uri, c.getLong(EmailContent.ID_PROJECTION_COLUMN)), 2165 values, null, null); 2166 result++; 2167 } 2168 db.setTransactionSuccessful(); 2169 } finally { 2170 db.endTransaction(); 2171 c.close(); 2172 } 2173 break outer; 2174 // Any cached table other than those above should be invalidated here 2175 case MESSAGE: 2176 // If we're doing some generic update, the whole cache needs to be 2177 // invalidated. This case should be quite rare 2178 cache.invalidate("Update", uri, selection); 2179 //$FALL-THROUGH$ 2180 default: 2181 result = db.update(tableName, values, selection, selectionArgs); 2182 break outer; 2183 } 2184 case ACCOUNT_RESET_NEW_COUNT_ID: 2185 id = uri.getPathSegments().get(1); 2186 if (cache != null) { 2187 cache.lock(id); 2188 } 2189 ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 2190 if (values != null) { 2191 Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME); 2192 if (set != null) { 2193 newMessageCount = new ContentValues(); 2194 newMessageCount.put(Account.NEW_MESSAGE_COUNT, set); 2195 } 2196 } 2197 try { 2198 result = db.update(tableName, newMessageCount, 2199 whereWithId(id, selection), selectionArgs); 2200 } finally { 2201 if (cache != null) { 2202 cache.unlock(id, values); 2203 } 2204 } 2205 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 2206 break; 2207 case ACCOUNT_RESET_NEW_COUNT: 2208 result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT, 2209 selection, selectionArgs); 2210 // Affects all accounts. Just invalidate all account cache. 2211 cache.invalidate("Reset all new counts", null, null); 2212 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 2213 break; 2214 default: 2215 throw new IllegalArgumentException("Unknown URI " + uri); 2216 } 2217 } catch (SQLiteException e) { 2218 checkDatabases(); 2219 throw e; 2220 } 2221 2222 // Notify all notifier cursors 2223 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id); 2224 2225 resolver.notifyChange(notificationUri, null); 2226 return result; 2227 } 2228 2229 /** 2230 * Returns the base notification URI for the given content type. 2231 * 2232 * @param match The type of content that was modified. 2233 */ 2234 private Uri getBaseNotificationUri(int match) { 2235 Uri baseUri = null; 2236 switch (match) { 2237 case MESSAGE: 2238 case MESSAGE_ID: 2239 case SYNCED_MESSAGE_ID: 2240 baseUri = Message.NOTIFIER_URI; 2241 break; 2242 case ACCOUNT: 2243 case ACCOUNT_ID: 2244 baseUri = Account.NOTIFIER_URI; 2245 break; 2246 } 2247 return baseUri; 2248 } 2249 2250 /** 2251 * Sends a change notification to any cursors observers of the given base URI. The final 2252 * notification URI is dynamically built to contain the specified information. It will be 2253 * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending 2254 * upon the given values. 2255 * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked. 2256 * If this is necessary, it can be added. However, due to the implementation of 2257 * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications. 2258 * 2259 * @param baseUri The base URI to send notifications to. Must be able to take appended IDs. 2260 * @param op Optional operation to be appended to the URI. 2261 * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be 2262 * appended to the base URI. 2263 */ 2264 private void sendNotifierChange(Uri baseUri, String op, String id) { 2265 if (baseUri == null) return; 2266 2267 final ContentResolver resolver = getContext().getContentResolver(); 2268 2269 // Append the operation, if specified 2270 if (op != null) { 2271 baseUri = baseUri.buildUpon().appendEncodedPath(op).build(); 2272 } 2273 2274 long longId = 0L; 2275 try { 2276 longId = Long.valueOf(id); 2277 } catch (NumberFormatException ignore) {} 2278 if (longId > 0) { 2279 resolver.notifyChange(ContentUris.withAppendedId(baseUri, longId), null); 2280 } else { 2281 resolver.notifyChange(baseUri, null); 2282 } 2283 } 2284 2285 @Override 2286 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 2287 throws OperationApplicationException { 2288 Context context = getContext(); 2289 SQLiteDatabase db = getDatabase(context); 2290 db.beginTransaction(); 2291 try { 2292 ContentProviderResult[] results = super.applyBatch(operations); 2293 db.setTransactionSuccessful(); 2294 return results; 2295 } finally { 2296 db.endTransaction(); 2297 } 2298 } 2299 2300 /** Counts the number of messages in each mailbox, and updates the message count column. */ 2301 @VisibleForTesting 2302 static void recalculateMessageCount(SQLiteDatabase db) { 2303 db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT + 2304 "= (select count(*) from " + Message.TABLE_NAME + 2305 " where " + Message.MAILBOX_KEY + " = " + 2306 Mailbox.TABLE_NAME + "." + EmailContent.RECORD_ID + ")"); 2307 } 2308 2309 @VisibleForTesting 2310 @SuppressWarnings("deprecation") 2311 static void convertPolicyFlagsToPolicyTable(SQLiteDatabase db) { 2312 Cursor c = db.query(Account.TABLE_NAME, 2313 new String[] {EmailContent.RECORD_ID /*0*/, AccountColumns.SECURITY_FLAGS /*1*/}, 2314 AccountColumns.SECURITY_FLAGS + ">0", null, null, null, null); 2315 ContentValues cv = new ContentValues(); 2316 String[] args = new String[1]; 2317 while (c.moveToNext()) { 2318 long securityFlags = c.getLong(1 /*SECURITY_FLAGS*/); 2319 Policy policy = LegacyPolicySet.flagsToPolicy(securityFlags); 2320 long policyId = db.insert(Policy.TABLE_NAME, null, policy.toContentValues()); 2321 cv.put(AccountColumns.POLICY_KEY, policyId); 2322 cv.putNull(AccountColumns.SECURITY_FLAGS); 2323 args[0] = Long.toString(c.getLong(0 /*RECORD_ID*/)); 2324 db.update(Account.TABLE_NAME, cv, EmailContent.RECORD_ID + "=?", args); 2325 } 2326 } 2327 2328 /** Upgrades the database from v17 to v18 */ 2329 @VisibleForTesting 2330 static void upgradeFromVersion17ToVersion18(SQLiteDatabase db) { 2331 // Copy the displayName column to the serverId column. In v18 of the database, 2332 // we use the serverId for IMAP/POP3 mailboxes instead of overloading the 2333 // display name. 2334 // 2335 // For posterity; this is the command we're executing: 2336 //sqlite> UPDATE mailbox SET serverid=displayname WHERE mailbox._id in ( 2337 // ...> SELECT mailbox._id FROM mailbox,account,hostauth WHERE 2338 // ...> mailbox.parentkey=0 AND mailbox.accountkey=account._id AND 2339 // ...> account.hostauthkeyrecv=hostauth._id AND 2340 // ...> (hostauth.protocol='imap' OR hostauth.protocol='pop3')); 2341 try { 2342 db.execSQL( 2343 "UPDATE " + Mailbox.TABLE_NAME + " SET " 2344 + MailboxColumns.SERVER_ID + "=" + MailboxColumns.DISPLAY_NAME 2345 + " WHERE " 2346 + Mailbox.TABLE_NAME + "." + MailboxColumns.ID + " IN ( SELECT " 2347 + Mailbox.TABLE_NAME + "." + MailboxColumns.ID + " FROM " 2348 + Mailbox.TABLE_NAME + "," + Account.TABLE_NAME + "," 2349 + HostAuth.TABLE_NAME + " WHERE " 2350 + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_KEY + "=0 AND " 2351 + Mailbox.TABLE_NAME + "." + MailboxColumns.ACCOUNT_KEY + "=" 2352 + Account.TABLE_NAME + "." + AccountColumns.ID + " AND " 2353 + Account.TABLE_NAME + "." + AccountColumns.HOST_AUTH_KEY_RECV + "=" 2354 + HostAuth.TABLE_NAME + "." + HostAuthColumns.ID + " AND ( " 2355 + HostAuth.TABLE_NAME + "." + HostAuthColumns.PROTOCOL + "='imap' OR " 2356 + HostAuth.TABLE_NAME + "." + HostAuthColumns.PROTOCOL + "='pop3' ) )"); 2357 } catch (SQLException e) { 2358 // Shouldn't be needed unless we're debugging and interrupt the process 2359 Log.w(TAG, "Exception upgrading EmailProvider.db from 17 to 18 " + e); 2360 } 2361 ContentCache.invalidateAllCaches(); 2362 } 2363 2364 /** Upgrades the database from v20 to v21 */ 2365 private static void upgradeFromVersion20ToVersion21(SQLiteDatabase db) { 2366 try { 2367 db.execSQL("alter table " + Mailbox.TABLE_NAME 2368 + " add column " + Mailbox.LAST_SEEN_MESSAGE_KEY + " integer;"); 2369 } catch (SQLException e) { 2370 // Shouldn't be needed unless we're debugging and interrupt the process 2371 Log.w(TAG, "Exception upgrading EmailProvider.db from 20 to 21 " + e); 2372 } 2373 } 2374 2375 /** 2376 * Upgrade the database from v21 to v22 2377 * This entails creating AccountManager accounts for all pop3 and imap accounts 2378 */ 2379 2380 private static final String[] RECV_PROJECTION = 2381 new String[] {AccountColumns.HOST_AUTH_KEY_RECV}; 2382 private static final int EMAIL_AND_RECV_COLUMN_RECV = 0; 2383 2384 static private void createAccountManagerAccount(Context context, HostAuth hostAuth) { 2385 AccountManager accountManager = AccountManager.get(context); 2386 android.accounts.Account amAccount = 2387 new android.accounts.Account(hostAuth.mLogin, AccountManagerTypes.TYPE_POP_IMAP); 2388 accountManager.addAccountExplicitly(amAccount, hostAuth.mPassword, null); 2389 ContentResolver.setIsSyncable(amAccount, EmailContent.AUTHORITY, 1); 2390 ContentResolver.setSyncAutomatically(amAccount, EmailContent.AUTHORITY, true); 2391 ContentResolver.setIsSyncable(amAccount, ContactsContract.AUTHORITY, 0); 2392 ContentResolver.setIsSyncable(amAccount, CalendarProviderStub.AUTHORITY, 0); 2393 } 2394 2395 @VisibleForTesting 2396 static void upgradeFromVersion21ToVersion22(SQLiteDatabase db, Context accountManagerContext) { 2397 try { 2398 // Loop through accounts, looking for pop/imap accounts 2399 Cursor accountCursor = db.query(Account.TABLE_NAME, RECV_PROJECTION, null, 2400 null, null, null, null); 2401 try { 2402 String[] hostAuthArgs = new String[1]; 2403 while (accountCursor.moveToNext()) { 2404 hostAuthArgs[0] = accountCursor.getString(EMAIL_AND_RECV_COLUMN_RECV); 2405 // Get the "receive" HostAuth for this account 2406 Cursor hostAuthCursor = db.query(HostAuth.TABLE_NAME, 2407 HostAuth.CONTENT_PROJECTION, HostAuth.RECORD_ID + "=?", hostAuthArgs, 2408 null, null, null); 2409 try { 2410 if (hostAuthCursor.moveToFirst()) { 2411 HostAuth hostAuth = new HostAuth(); 2412 hostAuth.restore(hostAuthCursor); 2413 String protocol = hostAuth.mProtocol; 2414 // If this is a pop3 or imap account, create the account manager account 2415 if ("imap".equals(protocol) || "pop3".equals(protocol)) { 2416 createAccountManagerAccount(accountManagerContext, hostAuth); 2417 } 2418 } 2419 } finally { 2420 hostAuthCursor.close(); 2421 } 2422 } 2423 } finally { 2424 accountCursor.close(); 2425 } 2426 } catch (SQLException e) { 2427 // Shouldn't be needed unless we're debugging and interrupt the process 2428 Log.w(TAG, "Exception upgrading EmailProvider.db from 20 to 21 " + e); 2429 } 2430 } 2431 2432 /** Upgrades the database from v22 to v23 */ 2433 private static void upgradeFromVersion22ToVersion23(SQLiteDatabase db) { 2434 try { 2435 db.execSQL("alter table " + Mailbox.TABLE_NAME 2436 + " add column " + Mailbox.LAST_TOUCHED_TIME + " integer default 0;"); 2437 } catch (SQLException e) { 2438 // Shouldn't be needed unless we're debugging and interrupt the process 2439 Log.w(TAG, "Exception upgrading EmailProvider.db from 22 to 23 " + e); 2440 } 2441 } 2442 2443 /** Adds in a column for information about a client certificate to use. */ 2444 private static void upgradeFromVersion23ToVersion24(SQLiteDatabase db) { 2445 try { 2446 db.execSQL("alter table " + HostAuth.TABLE_NAME 2447 + " add column " + HostAuth.CLIENT_CERT_ALIAS + " text;"); 2448 } catch (SQLException e) { 2449 // Shouldn't be needed unless we're debugging and interrupt the process 2450 Log.w(TAG, "Exception upgrading EmailProvider.db from 23 to 24 " + e); 2451 } 2452 } 2453 2454 /** Upgrades the database from v24 to v25 by creating table for quick responses */ 2455 private static void upgradeFromVersion24ToVersion25(SQLiteDatabase db) { 2456 try { 2457 createQuickResponseTable(db); 2458 } catch (SQLException e) { 2459 // Shouldn't be needed unless we're debugging and interrupt the process 2460 Log.w(TAG, "Exception upgrading EmailProvider.db from 24 to 25 " + e); 2461 } 2462 } 2463 2464 2465 private static final String[] ID_FLAGS_RECV_PROJECTION = 2466 new String[] {AccountColumns.ID, AccountColumns.FLAGS, AccountColumns.HOST_AUTH_KEY_RECV}; 2467 private static final int ID_FLAGS_RECV_COLUMN_ID = 0; 2468 private static final int ID_FLAGS_RECV_COLUMN_FLAGS = 1; 2469 private static final int ID_FLAGS_RECV_COLUMN_RECV = 2; 2470 2471 /** Upgrades the database from v25 to v26 by adding FLAG_SUPPORTS_SEARCH to IMAP accounts */ 2472 private static void upgradeFromVersion25ToVersion26(SQLiteDatabase db) { 2473 try { 2474 // Loop through accounts, looking for imap accounts 2475 Cursor accountCursor = db.query(Account.TABLE_NAME, ID_FLAGS_RECV_PROJECTION, null, 2476 null, null, null, null); 2477 ContentValues cv = new ContentValues(); 2478 try { 2479 String[] hostAuthArgs = new String[1]; 2480 while (accountCursor.moveToNext()) { 2481 hostAuthArgs[0] = accountCursor.getString(ID_FLAGS_RECV_COLUMN_RECV); 2482 // Get the "receive" HostAuth for this account 2483 Cursor hostAuthCursor = db.query(HostAuth.TABLE_NAME, 2484 HostAuth.CONTENT_PROJECTION, HostAuth.RECORD_ID + "=?", hostAuthArgs, 2485 null, null, null); 2486 try { 2487 if (hostAuthCursor.moveToFirst()) { 2488 HostAuth hostAuth = new HostAuth(); 2489 hostAuth.restore(hostAuthCursor); 2490 // If this is an imap account, add the search flag 2491 if (HostAuth.SCHEME_IMAP.equals(hostAuth.mProtocol)) { 2492 String id = accountCursor.getString(ID_FLAGS_RECV_COLUMN_ID); 2493 int flags = accountCursor.getInt(ID_FLAGS_RECV_COLUMN_FLAGS); 2494 cv.put(AccountColumns.FLAGS, flags | Account.FLAGS_SUPPORTS_SEARCH); 2495 db.update(Account.TABLE_NAME, cv, Account.RECORD_ID + "=?", 2496 new String[] {id}); 2497 } 2498 } 2499 } finally { 2500 hostAuthCursor.close(); 2501 } 2502 } 2503 } finally { 2504 accountCursor.close(); 2505 } 2506 } catch (SQLException e) { 2507 // Shouldn't be needed unless we're debugging and interrupt the process 2508 Log.w(TAG, "Exception upgrading EmailProvider.db from 25 to 26 " + e); 2509 } 2510 } 2511 2512 /** 2513 * For testing purposes, check whether a given row is cached 2514 * @param baseUri the base uri of the EmailContent 2515 * @param id the row id of the EmailContent 2516 * @return whether or not the row is currently cached 2517 */ 2518 @VisibleForTesting 2519 protected boolean isCached(Uri baseUri, long id) { 2520 int match = findMatch(baseUri, "isCached"); 2521 int table = match >> BASE_SHIFT; 2522 ContentCache cache = mContentCaches[table]; 2523 if (cache == null) return false; 2524 Cursor cc = cache.get(Long.toString(id)); 2525 return (cc != null); 2526 } 2527} 2528