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