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