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