EmailProvider.java revision 1c1bd6a3eb2b7d99e30713648616c807ae20cbb5
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 com.android.email.Email; 20import com.android.email.provider.ContentCache.CacheToken; 21import com.android.email.provider.EmailContent.Account; 22import com.android.email.provider.EmailContent.AccountColumns; 23import com.android.email.provider.EmailContent.Attachment; 24import com.android.email.provider.EmailContent.AttachmentColumns; 25import com.android.email.provider.EmailContent.Body; 26import com.android.email.provider.EmailContent.BodyColumns; 27import com.android.email.provider.EmailContent.HostAuth; 28import com.android.email.provider.EmailContent.HostAuthColumns; 29import com.android.email.provider.EmailContent.Mailbox; 30import com.android.email.provider.EmailContent.MailboxColumns; 31import com.android.email.provider.EmailContent.Message; 32import com.android.email.provider.EmailContent.MessageColumns; 33import com.android.email.provider.EmailContent.SyncColumns; 34import com.android.email.service.AttachmentDownloadService; 35 36import android.accounts.AccountManager; 37import android.content.ContentProvider; 38import android.content.ContentProviderOperation; 39import android.content.ContentProviderResult; 40import android.content.ContentResolver; 41import android.content.ContentUris; 42import android.content.ContentValues; 43import android.content.Context; 44import android.content.OperationApplicationException; 45import android.content.UriMatcher; 46import android.database.Cursor; 47import android.database.SQLException; 48import android.database.sqlite.SQLiteDatabase; 49import android.database.sqlite.SQLiteException; 50import android.database.sqlite.SQLiteOpenHelper; 51import android.net.Uri; 52import android.util.Log; 53 54import java.io.File; 55import java.util.ArrayList; 56 57public class EmailProvider extends ContentProvider { 58 59 private static final String TAG = "EmailProvider"; 60 61 protected static final String DATABASE_NAME = "EmailProvider.db"; 62 protected static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; 63 64 public static final String ACTION_ATTACHMENT_UPDATED = "com.android.email.ATTACHMENT_UPDATED"; 65 public static final String ATTACHMENT_UPDATED_EXTRA_FLAGS = 66 "com.android.email.ATTACHMENT_UPDATED_FLAGS"; 67 68 public static final String EMAIL_MESSAGE_MIME_TYPE = 69 "vnd.android.cursor.item/email-message"; 70 public static final String EMAIL_ATTACHMENT_MIME_TYPE = 71 "vnd.android.cursor.item/email-attachment"; 72 73 public static final Uri INTEGRITY_CHECK_URI = 74 Uri.parse("content://" + EmailContent.AUTHORITY + "/integrityCheck"); 75 76 // Definitions for our queries looking for orphaned messages 77 private static final String[] ORPHANS_PROJECTION 78 = new String[] {MessageColumns.ID, MessageColumns.MAILBOX_KEY}; 79 private static final int ORPHANS_ID = 0; 80 private static final int ORPHANS_MAILBOX_KEY = 1; 81 82 private static final String WHERE_ID = EmailContent.RECORD_ID + "=?"; 83 84 // We'll cache the following four tables; sizes are best estimates of effective values 85 private static final ContentCache sCacheAccount = 86 new ContentCache("Account", Account.CONTENT_PROJECTION, 4); 87 private static final ContentCache sCacheHostAuth = 88 new ContentCache("HostAuth", HostAuth.CONTENT_PROJECTION, 8); 89 /*package*/ static final ContentCache sCacheMailbox = 90 new ContentCache("Mailbox", Mailbox.CONTENT_PROJECTION, 8); 91 private static final ContentCache sCacheMessage = 92 new ContentCache("Message", Message.CONTENT_PROJECTION, 3); 93 94 // Any changes to the database format *must* include update-in-place code. 95 // Original version: 3 96 // Version 4: Database wipe required; changing AccountManager interface w/Exchange 97 // Version 5: Database wipe required; changing AccountManager interface w/Exchange 98 // Version 6: Adding Message.mServerTimeStamp column 99 // Version 7: Replace the mailbox_delete trigger with a version that removes orphaned messages 100 // from the Message_Deletes and Message_Updates tables 101 // Version 8: Add security flags column to accounts table 102 // Version 9: Add security sync key and signature to accounts table 103 // Version 10: Add meeting info to message table 104 // Version 11: Add content and flags to attachment table 105 // Version 12: Add content_bytes to attachment table. content is deprecated. 106 // Version 13: Add messageCount to Mailbox table. 107 // Version 14: Add snippet to Message table 108 // Version 15: Fix upgrade problem in version 14. 109 // Version 16: Add accountKey to Attachment table 110 public static final int DATABASE_VERSION = 16; 111 112 // Any changes to the database format *must* include update-in-place code. 113 // Original version: 2 114 // Version 3: Add "sourceKey" column 115 // Version 4: Database wipe required; changing AccountManager interface w/Exchange 116 // Version 5: Database wipe required; changing AccountManager interface w/Exchange 117 // Version 6: Adding Body.mIntroText column 118 public static final int BODY_DATABASE_VERSION = 6; 119 120 public static final String EMAIL_AUTHORITY = "com.android.email.provider"; 121 // The notifier authority is used to send notifications regarding changes to messages (insert, 122 // delete, or update) and is intended as an optimization for use by clients of message list 123 // cursors (initially, the email AppWidget). 124 public static final String EMAIL_NOTIFIER_AUTHORITY = "com.android.email.notifier"; 125 126 private static final int ACCOUNT_BASE = 0; 127 private static final int ACCOUNT = ACCOUNT_BASE; 128 private static final int ACCOUNT_ID = ACCOUNT_BASE + 1; 129 private static final int ACCOUNT_ID_ADD_TO_FIELD = ACCOUNT_BASE + 2; 130 private static final int ACCOUNT_RESET_NEW_COUNT = ACCOUNT_BASE + 3; 131 private static final int ACCOUNT_RESET_NEW_COUNT_ID = ACCOUNT_BASE + 4; 132 133 private static final int MAILBOX_BASE = 0x1000; 134 private static final int MAILBOX = MAILBOX_BASE; 135 private static final int MAILBOX_ID = MAILBOX_BASE + 1; 136 private static final int MAILBOX_ID_ADD_TO_FIELD = MAILBOX_BASE + 2; 137 138 private static final int MESSAGE_BASE = 0x2000; 139 private static final int MESSAGE = MESSAGE_BASE; 140 private static final int MESSAGE_ID = MESSAGE_BASE + 1; 141 private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2; 142 143 private static final int ATTACHMENT_BASE = 0x3000; 144 private static final int ATTACHMENT = ATTACHMENT_BASE; 145 private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1; 146 private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2; 147 148 private static final int HOSTAUTH_BASE = 0x4000; 149 private static final int HOSTAUTH = HOSTAUTH_BASE; 150 private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1; 151 152 private static final int UPDATED_MESSAGE_BASE = 0x5000; 153 private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE; 154 private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1; 155 156 private static final int DELETED_MESSAGE_BASE = 0x6000; 157 private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE; 158 private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1; 159 160 // MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS 161 private static final int LAST_EMAIL_PROVIDER_DB_BASE = DELETED_MESSAGE_BASE; 162 163 // DO NOT CHANGE BODY_BASE!! 164 private static final int BODY_BASE = LAST_EMAIL_PROVIDER_DB_BASE + 0x1000; 165 private static final int BODY = BODY_BASE; 166 private static final int BODY_ID = BODY_BASE + 1; 167 168 private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc. 169 170 // TABLE_NAMES MUST remain in the order of the BASE constants above (e.g. ACCOUNT_BASE = 0x0000, 171 // MESSAGE_BASE = 0x1000, etc.) 172 private static final String[] TABLE_NAMES = { 173 EmailContent.Account.TABLE_NAME, 174 EmailContent.Mailbox.TABLE_NAME, 175 EmailContent.Message.TABLE_NAME, 176 EmailContent.Attachment.TABLE_NAME, 177 EmailContent.HostAuth.TABLE_NAME, 178 EmailContent.Message.UPDATED_TABLE_NAME, 179 EmailContent.Message.DELETED_TABLE_NAME, 180 EmailContent.Body.TABLE_NAME 181 }; 182 183 // CONTENT_CACHES MUST remain in the order of the BASE constants above 184 private static final ContentCache[] CONTENT_CACHES = { 185 sCacheAccount, 186 sCacheMailbox, 187 sCacheMessage, 188 null, 189 sCacheHostAuth, 190 null, 191 null, 192 null}; 193 194 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 195 196 /** 197 * Let's only generate these SQL strings once, as they are used frequently 198 * Note that this isn't relevant for table creation strings, since they are used only once 199 */ 200 private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " + 201 Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 202 EmailContent.RECORD_ID + '='; 203 204 private static final String UPDATED_MESSAGE_DELETE = "delete from " + 205 Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '='; 206 207 private static final String DELETED_MESSAGE_INSERT = "insert or replace into " + 208 Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 209 EmailContent.RECORD_ID + '='; 210 211 private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME + 212 " where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY + 213 " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " + 214 Message.TABLE_NAME + ')'; 215 216 private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME + 217 " where " + BodyColumns.MESSAGE_KEY + '='; 218 219 private static final String ID_EQUALS = EmailContent.RECORD_ID + "=?"; 220 221 private static final String TRIGGER_MAILBOX_DELETE = 222 "create trigger mailbox_delete before delete on " + Mailbox.TABLE_NAME + 223 " begin" + 224 " delete from " + Message.TABLE_NAME + 225 " where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID + 226 "; delete from " + Message.UPDATED_TABLE_NAME + 227 " where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID + 228 "; delete from " + Message.DELETED_TABLE_NAME + 229 " where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID + 230 "; end"; 231 232 private static final ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 233 234 public static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; 235 236 static { 237 // Email URI matching table 238 UriMatcher matcher = sURIMatcher; 239 240 // All accounts 241 matcher.addURI(EMAIL_AUTHORITY, "account", ACCOUNT); 242 // A specific account 243 // insert into this URI causes a mailbox to be added to the account 244 matcher.addURI(EMAIL_AUTHORITY, "account/#", ACCOUNT_ID); 245 246 // Special URI to reset the new message count. Only update works, and content values 247 // will be ignored. 248 matcher.addURI(EMAIL_AUTHORITY, "resetNewMessageCount", ACCOUNT_RESET_NEW_COUNT); 249 matcher.addURI(EMAIL_AUTHORITY, "resetNewMessageCount/#", ACCOUNT_RESET_NEW_COUNT_ID); 250 251 // All mailboxes 252 matcher.addURI(EMAIL_AUTHORITY, "mailbox", MAILBOX); 253 // A specific mailbox 254 // insert into this URI causes a message to be added to the mailbox 255 // ** NOTE For now, the accountKey must be set manually in the values! 256 matcher.addURI(EMAIL_AUTHORITY, "mailbox/#", MAILBOX_ID); 257 258 // All messages 259 matcher.addURI(EMAIL_AUTHORITY, "message", MESSAGE); 260 // A specific message 261 // insert into this URI causes an attachment to be added to the message 262 matcher.addURI(EMAIL_AUTHORITY, "message/#", MESSAGE_ID); 263 264 // A specific attachment 265 matcher.addURI(EMAIL_AUTHORITY, "attachment", ATTACHMENT); 266 // A specific attachment (the header information) 267 matcher.addURI(EMAIL_AUTHORITY, "attachment/#", ATTACHMENT_ID); 268 // The attachments of a specific message (query only) (insert & delete TBD) 269 matcher.addURI(EMAIL_AUTHORITY, "attachment/message/#", ATTACHMENTS_MESSAGE_ID); 270 271 // All mail bodies 272 matcher.addURI(EMAIL_AUTHORITY, "body", BODY); 273 // A specific mail body 274 matcher.addURI(EMAIL_AUTHORITY, "body/#", BODY_ID); 275 276 // All hostauth records 277 matcher.addURI(EMAIL_AUTHORITY, "hostauth", HOSTAUTH); 278 // A specific hostauth 279 matcher.addURI(EMAIL_AUTHORITY, "hostauth/#", HOSTAUTH_ID); 280 281 // Atomically a constant value to a particular field of a mailbox/account 282 matcher.addURI(EMAIL_AUTHORITY, "mailboxIdAddToField/#", MAILBOX_ID_ADD_TO_FIELD); 283 matcher.addURI(EMAIL_AUTHORITY, "accountIdAddToField/#", ACCOUNT_ID_ADD_TO_FIELD); 284 285 /** 286 * THIS URI HAS SPECIAL SEMANTICS 287 * ITS USE IS INTENDED FOR THE UI APPLICATION TO MARK CHANGES THAT NEED TO BE SYNCED BACK 288 * TO A SERVER VIA A SYNC ADAPTER 289 */ 290 matcher.addURI(EMAIL_AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); 291 292 /** 293 * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY 294 * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI 295 * BY THE UI APPLICATION 296 */ 297 // All deleted messages 298 matcher.addURI(EMAIL_AUTHORITY, "deletedMessage", DELETED_MESSAGE); 299 // A specific deleted message 300 matcher.addURI(EMAIL_AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID); 301 302 // All updated messages 303 matcher.addURI(EMAIL_AUTHORITY, "updatedMessage", UPDATED_MESSAGE); 304 // A specific updated message 305 matcher.addURI(EMAIL_AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID); 306 307 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues(); 308 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0); 309 } 310 311 312 /** 313 * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in 314 * @param uri the Uri to match 315 * @return the match value 316 */ 317 private static int findMatch(Uri uri, String methodName) { 318 int match = sURIMatcher.match(uri); 319 if (match < 0) { 320 throw new IllegalArgumentException("Unknown uri: " + uri); 321 } else if (Email.LOGD) { 322 Log.v(TAG, methodName + ": uri=" + uri + ", match is " + match); 323 } 324 return match; 325 } 326 327 /* 328 * Internal helper method for index creation. 329 * Example: 330 * "create index message_" + MessageColumns.FLAG_READ 331 * + " on " + Message.TABLE_NAME + " (" + MessageColumns.FLAG_READ + ");" 332 */ 333 /* package */ 334 static String createIndex(String tableName, String columnName) { 335 return "create index " + tableName.toLowerCase() + '_' + columnName 336 + " on " + tableName + " (" + columnName + ");"; 337 } 338 339 static void createMessageTable(SQLiteDatabase db) { 340 String messageColumns = MessageColumns.DISPLAY_NAME + " text, " 341 + MessageColumns.TIMESTAMP + " integer, " 342 + MessageColumns.SUBJECT + " text, " 343 + MessageColumns.FLAG_READ + " integer, " 344 + MessageColumns.FLAG_LOADED + " integer, " 345 + MessageColumns.FLAG_FAVORITE + " integer, " 346 + MessageColumns.FLAG_ATTACHMENT + " integer, " 347 + MessageColumns.FLAGS + " integer, " 348 + MessageColumns.CLIENT_ID + " integer, " 349 + MessageColumns.MESSAGE_ID + " text, " 350 + MessageColumns.MAILBOX_KEY + " integer, " 351 + MessageColumns.ACCOUNT_KEY + " integer, " 352 + MessageColumns.FROM_LIST + " text, " 353 + MessageColumns.TO_LIST + " text, " 354 + MessageColumns.CC_LIST + " text, " 355 + MessageColumns.BCC_LIST + " text, " 356 + MessageColumns.REPLY_TO_LIST + " text, " 357 + MessageColumns.MEETING_INFO + " text, " 358 + MessageColumns.SNIPPET + " text" 359 + ");"; 360 361 // This String and the following String MUST have the same columns, except for the type 362 // of those columns! 363 String createString = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 364 + SyncColumns.SERVER_ID + " text, " 365 + SyncColumns.SERVER_TIMESTAMP + " integer, " 366 + messageColumns; 367 368 // For the updated and deleted tables, the id is assigned, but we do want to keep track 369 // of the ORDER of updates using an autoincrement primary key. We use the DATA column 370 // at this point; it has no other function 371 String altCreateString = " (" + EmailContent.RECORD_ID + " integer unique, " 372 + SyncColumns.SERVER_ID + " text, " 373 + SyncColumns.SERVER_TIMESTAMP + " integer, " 374 + messageColumns; 375 376 // The three tables have the same schema 377 db.execSQL("create table " + Message.TABLE_NAME + createString); 378 db.execSQL("create table " + Message.UPDATED_TABLE_NAME + altCreateString); 379 db.execSQL("create table " + Message.DELETED_TABLE_NAME + altCreateString); 380 381 String indexColumns[] = { 382 MessageColumns.TIMESTAMP, 383 MessageColumns.FLAG_READ, 384 MessageColumns.FLAG_LOADED, 385 MessageColumns.MAILBOX_KEY, 386 SyncColumns.SERVER_ID 387 }; 388 389 for (String columnName : indexColumns) { 390 db.execSQL(createIndex(Message.TABLE_NAME, columnName)); 391 } 392 393 // Deleting a Message deletes all associated Attachments 394 // Deleting the associated Body cannot be done in a trigger, because the Body is stored 395 // in a separate database, and trigger cannot operate on attached databases. 396 db.execSQL("create trigger message_delete before delete on " + Message.TABLE_NAME + 397 " begin delete from " + Attachment.TABLE_NAME + 398 " where " + AttachmentColumns.MESSAGE_KEY + "=old." + EmailContent.RECORD_ID + 399 "; end"); 400 401 // Add triggers to keep unread count accurate per mailbox 402 403 // NOTE: SQLite's before triggers are not safe when recursive triggers are involved. 404 // Use caution when changing them. 405 406 // Insert a message; if flagRead is zero, add to the unread count of the message's mailbox 407 db.execSQL("create trigger unread_message_insert before insert on " + Message.TABLE_NAME + 408 " when NEW." + MessageColumns.FLAG_READ + "=0" + 409 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 410 '=' + MailboxColumns.UNREAD_COUNT + "+1" + 411 " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY + 412 "; end"); 413 414 // Delete a message; if flagRead is zero, decrement the unread count of the msg's mailbox 415 db.execSQL("create trigger unread_message_delete before delete on " + Message.TABLE_NAME + 416 " when OLD." + MessageColumns.FLAG_READ + "=0" + 417 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 418 '=' + MailboxColumns.UNREAD_COUNT + "-1" + 419 " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY + 420 "; end"); 421 422 // Change a message's mailbox 423 db.execSQL("create trigger unread_message_move before update of " + 424 MessageColumns.MAILBOX_KEY + " on " + Message.TABLE_NAME + 425 " when OLD." + MessageColumns.FLAG_READ + "=0" + 426 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 427 '=' + MailboxColumns.UNREAD_COUNT + "-1" + 428 " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY + 429 "; update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 430 '=' + MailboxColumns.UNREAD_COUNT + "+1" + 431 " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY + 432 "; end"); 433 434 // Change a message's read state 435 db.execSQL("create trigger unread_message_read before update of " + 436 MessageColumns.FLAG_READ + " on " + Message.TABLE_NAME + 437 " when OLD." + MessageColumns.FLAG_READ + "!=NEW." + MessageColumns.FLAG_READ + 438 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 439 '=' + MailboxColumns.UNREAD_COUNT + "+ case OLD." + MessageColumns.FLAG_READ + 440 " when 0 then -1 else 1 end" + 441 " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY + 442 "; end"); 443 444 // Add triggers to update message count per mailbox 445 446 // Insert a message. 447 db.execSQL("create trigger message_count_message_insert after insert on " + 448 Message.TABLE_NAME + 449 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT + 450 '=' + MailboxColumns.MESSAGE_COUNT + "+1" + 451 " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY + 452 "; end"); 453 454 // Delete a message; if flagRead is zero, decrement the unread count of the msg's mailbox 455 db.execSQL("create trigger message_count_message_delete after delete on " + 456 Message.TABLE_NAME + 457 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT + 458 '=' + MailboxColumns.MESSAGE_COUNT + "-1" + 459 " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY + 460 "; end"); 461 462 // Change a message's mailbox 463 db.execSQL("create trigger message_count_message_move after update of " + 464 MessageColumns.MAILBOX_KEY + " on " + Message.TABLE_NAME + 465 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT + 466 '=' + MailboxColumns.MESSAGE_COUNT + "-1" + 467 " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY + 468 "; update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT + 469 '=' + MailboxColumns.MESSAGE_COUNT + "+1" + 470 " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY + 471 "; end"); 472 } 473 474 static void resetMessageTable(SQLiteDatabase db, int oldVersion, int newVersion) { 475 try { 476 db.execSQL("drop table " + Message.TABLE_NAME); 477 db.execSQL("drop table " + Message.UPDATED_TABLE_NAME); 478 db.execSQL("drop table " + Message.DELETED_TABLE_NAME); 479 } catch (SQLException e) { 480 } 481 createMessageTable(db); 482 } 483 484 static void createAccountTable(SQLiteDatabase db) { 485 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 486 + AccountColumns.DISPLAY_NAME + " text, " 487 + AccountColumns.EMAIL_ADDRESS + " text, " 488 + AccountColumns.SYNC_KEY + " text, " 489 + AccountColumns.SYNC_LOOKBACK + " integer, " 490 + AccountColumns.SYNC_INTERVAL + " text, " 491 + AccountColumns.HOST_AUTH_KEY_RECV + " integer, " 492 + AccountColumns.HOST_AUTH_KEY_SEND + " integer, " 493 + AccountColumns.FLAGS + " integer, " 494 + AccountColumns.IS_DEFAULT + " integer, " 495 + AccountColumns.COMPATIBILITY_UUID + " text, " 496 + AccountColumns.SENDER_NAME + " text, " 497 + AccountColumns.RINGTONE_URI + " text, " 498 + AccountColumns.PROTOCOL_VERSION + " text, " 499 + AccountColumns.NEW_MESSAGE_COUNT + " integer, " 500 + AccountColumns.SECURITY_FLAGS + " integer, " 501 + AccountColumns.SECURITY_SYNC_KEY + " text, " 502 + AccountColumns.SIGNATURE + " text " 503 + ");"; 504 db.execSQL("create table " + Account.TABLE_NAME + s); 505 // Deleting an account deletes associated Mailboxes and HostAuth's 506 db.execSQL("create trigger account_delete before delete on " + Account.TABLE_NAME + 507 " begin delete from " + Mailbox.TABLE_NAME + 508 " where " + MailboxColumns.ACCOUNT_KEY + "=old." + EmailContent.RECORD_ID + 509 "; delete from " + HostAuth.TABLE_NAME + 510 " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_RECV + 511 "; delete from " + HostAuth.TABLE_NAME + 512 " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_SEND + 513 "; end"); 514 } 515 516 static void resetAccountTable(SQLiteDatabase db, int oldVersion, int newVersion) { 517 try { 518 db.execSQL("drop table " + Account.TABLE_NAME); 519 } catch (SQLException e) { 520 } 521 createAccountTable(db); 522 } 523 524 static void createHostAuthTable(SQLiteDatabase db) { 525 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 526 + HostAuthColumns.PROTOCOL + " text, " 527 + HostAuthColumns.ADDRESS + " text, " 528 + HostAuthColumns.PORT + " integer, " 529 + HostAuthColumns.FLAGS + " integer, " 530 + HostAuthColumns.LOGIN + " text, " 531 + HostAuthColumns.PASSWORD + " text, " 532 + HostAuthColumns.DOMAIN + " text, " 533 + HostAuthColumns.ACCOUNT_KEY + " integer" 534 + ");"; 535 db.execSQL("create table " + HostAuth.TABLE_NAME + s); 536 } 537 538 static void resetHostAuthTable(SQLiteDatabase db, int oldVersion, int newVersion) { 539 try { 540 db.execSQL("drop table " + HostAuth.TABLE_NAME); 541 } catch (SQLException e) { 542 } 543 createHostAuthTable(db); 544 } 545 546 static void createMailboxTable(SQLiteDatabase db) { 547 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 548 + MailboxColumns.DISPLAY_NAME + " text, " 549 + MailboxColumns.SERVER_ID + " text, " 550 + MailboxColumns.PARENT_SERVER_ID + " text, " 551 + MailboxColumns.ACCOUNT_KEY + " integer, " 552 + MailboxColumns.TYPE + " integer, " 553 + MailboxColumns.DELIMITER + " integer, " 554 + MailboxColumns.SYNC_KEY + " text, " 555 + MailboxColumns.SYNC_LOOKBACK + " integer, " 556 + MailboxColumns.SYNC_INTERVAL + " integer, " 557 + MailboxColumns.SYNC_TIME + " integer, " 558 + MailboxColumns.UNREAD_COUNT + " integer, " 559 + MailboxColumns.FLAG_VISIBLE + " integer, " 560 + MailboxColumns.FLAGS + " integer, " 561 + MailboxColumns.VISIBLE_LIMIT + " integer, " 562 + MailboxColumns.SYNC_STATUS + " text, " 563 + MailboxColumns.MESSAGE_COUNT + " integer not null default 0" 564 + ");"; 565 db.execSQL("create table " + Mailbox.TABLE_NAME + s); 566 db.execSQL("create index mailbox_" + MailboxColumns.SERVER_ID 567 + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.SERVER_ID + ")"); 568 db.execSQL("create index mailbox_" + MailboxColumns.ACCOUNT_KEY 569 + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.ACCOUNT_KEY + ")"); 570 // Deleting a Mailbox deletes associated Messages in all three tables 571 db.execSQL(TRIGGER_MAILBOX_DELETE); 572 } 573 574 static void resetMailboxTable(SQLiteDatabase db, int oldVersion, int newVersion) { 575 try { 576 db.execSQL("drop table " + Mailbox.TABLE_NAME); 577 } catch (SQLException e) { 578 } 579 createMailboxTable(db); 580 } 581 582 static void createAttachmentTable(SQLiteDatabase db) { 583 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 584 + AttachmentColumns.FILENAME + " text, " 585 + AttachmentColumns.MIME_TYPE + " text, " 586 + AttachmentColumns.SIZE + " integer, " 587 + AttachmentColumns.CONTENT_ID + " text, " 588 + AttachmentColumns.CONTENT_URI + " text, " 589 + AttachmentColumns.MESSAGE_KEY + " integer, " 590 + AttachmentColumns.LOCATION + " text, " 591 + AttachmentColumns.ENCODING + " text, " 592 + AttachmentColumns.CONTENT + " text, " 593 + AttachmentColumns.FLAGS + " integer, " 594 + AttachmentColumns.CONTENT_BYTES + " blob, " 595 + AttachmentColumns.ACCOUNT_KEY + " integer" 596 + ");"; 597 db.execSQL("create table " + Attachment.TABLE_NAME + s); 598 db.execSQL(createIndex(Attachment.TABLE_NAME, AttachmentColumns.MESSAGE_KEY)); 599 } 600 601 static void resetAttachmentTable(SQLiteDatabase db, int oldVersion, int newVersion) { 602 try { 603 db.execSQL("drop table " + Attachment.TABLE_NAME); 604 } catch (SQLException e) { 605 } 606 createAttachmentTable(db); 607 } 608 609 static void createBodyTable(SQLiteDatabase db) { 610 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 611 + BodyColumns.MESSAGE_KEY + " integer, " 612 + BodyColumns.HTML_CONTENT + " text, " 613 + BodyColumns.TEXT_CONTENT + " text, " 614 + BodyColumns.HTML_REPLY + " text, " 615 + BodyColumns.TEXT_REPLY + " text, " 616 + BodyColumns.SOURCE_MESSAGE_KEY + " text, " 617 + BodyColumns.INTRO_TEXT + " text" 618 + ");"; 619 db.execSQL("create table " + Body.TABLE_NAME + s); 620 db.execSQL(createIndex(Body.TABLE_NAME, BodyColumns.MESSAGE_KEY)); 621 } 622 623 static void upgradeBodyTable(SQLiteDatabase db, int oldVersion, int newVersion) { 624 if (oldVersion < 5) { 625 try { 626 db.execSQL("drop table " + Body.TABLE_NAME); 627 createBodyTable(db); 628 } catch (SQLException e) { 629 } 630 } else if (oldVersion == 5) { 631 try { 632 db.execSQL("alter table " + Body.TABLE_NAME 633 + " add " + BodyColumns.INTRO_TEXT + " text"); 634 } catch (SQLException e) { 635 // Shouldn't be needed unless we're debugging and interrupt the process 636 Log.w(TAG, "Exception upgrading EmailProviderBody.db from v5 to v6", e); 637 } 638 oldVersion = 6; 639 } 640 } 641 642 private SQLiteDatabase mDatabase; 643 private SQLiteDatabase mBodyDatabase; 644 645 public synchronized SQLiteDatabase getDatabase(Context context) { 646 // Always return the cached database, if we've got one 647 if (mDatabase != null) { 648 return mDatabase; 649 } 650 651 // Whenever we create or re-cache the databases, make sure that we haven't lost one 652 // to corruption 653 checkDatabases(); 654 655 DatabaseHelper helper = new DatabaseHelper(context, DATABASE_NAME); 656 mDatabase = helper.getWritableDatabase(); 657 if (mDatabase != null) { 658 mDatabase.setLockingEnabled(true); 659 BodyDatabaseHelper bodyHelper = new BodyDatabaseHelper(context, BODY_DATABASE_NAME); 660 mBodyDatabase = bodyHelper.getWritableDatabase(); 661 if (mBodyDatabase != null) { 662 mBodyDatabase.setLockingEnabled(true); 663 String bodyFileName = mBodyDatabase.getPath(); 664 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); 665 } 666 } 667 668 // Check for any orphaned Messages in the updated/deleted tables 669 deleteOrphans(mDatabase, Message.UPDATED_TABLE_NAME); 670 deleteOrphans(mDatabase, Message.DELETED_TABLE_NAME); 671 672 return mDatabase; 673 } 674 675 /*package*/ static SQLiteDatabase getReadableDatabase(Context context) { 676 DatabaseHelper helper = new EmailProvider().new DatabaseHelper(context, DATABASE_NAME); 677 return helper.getReadableDatabase(); 678 } 679 680 /** {@inheritDoc} */ 681 @Override 682 public void shutdown() { 683 if (mDatabase != null) { 684 mDatabase.close(); 685 mDatabase = null; 686 } 687 if (mBodyDatabase != null) { 688 mBodyDatabase.close(); 689 mBodyDatabase = null; 690 } 691 } 692 693 /*package*/ static void deleteOrphans(SQLiteDatabase database, String tableName) { 694 if (database != null) { 695 // We'll look at all of the items in the table; there won't be many typically 696 Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); 697 // Usually, there will be nothing in these tables, so make a quick check 698 try { 699 if (c.getCount() == 0) return; 700 ArrayList<Long> foundMailboxes = new ArrayList<Long>(); 701 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>(); 702 ArrayList<Long> deleteList = new ArrayList<Long>(); 703 String[] bindArray = new String[1]; 704 while (c.moveToNext()) { 705 // Get the mailbox key and see if we've already found this mailbox 706 // If so, we're fine 707 long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY); 708 // If we already know this mailbox doesn't exist, mark the message for deletion 709 if (notFoundMailboxes.contains(mailboxId)) { 710 deleteList.add(c.getLong(ORPHANS_ID)); 711 // If we don't know about this mailbox, we'll try to find it 712 } else if (!foundMailboxes.contains(mailboxId)) { 713 bindArray[0] = Long.toString(mailboxId); 714 Cursor boxCursor = database.query(Mailbox.TABLE_NAME, 715 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null); 716 try { 717 // If it exists, we'll add it to the "found" mailboxes 718 if (boxCursor.moveToFirst()) { 719 foundMailboxes.add(mailboxId); 720 // Otherwise, we'll add to "not found" and mark the message for deletion 721 } else { 722 notFoundMailboxes.add(mailboxId); 723 deleteList.add(c.getLong(ORPHANS_ID)); 724 } 725 } finally { 726 boxCursor.close(); 727 } 728 } 729 } 730 // Now, delete the orphan messages 731 for (long messageId: deleteList) { 732 bindArray[0] = Long.toString(messageId); 733 database.delete(tableName, WHERE_ID, bindArray); 734 } 735 } finally { 736 c.close(); 737 } 738 } 739 } 740 741 private class BodyDatabaseHelper extends SQLiteOpenHelper { 742 BodyDatabaseHelper(Context context, String name) { 743 super(context, name, null, BODY_DATABASE_VERSION); 744 } 745 746 @Override 747 public void onCreate(SQLiteDatabase db) { 748 Log.d(TAG, "Creating EmailProviderBody database"); 749 createBodyTable(db); 750 } 751 752 @Override 753 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 754 upgradeBodyTable(db, oldVersion, newVersion); 755 } 756 757 @Override 758 public void onOpen(SQLiteDatabase db) { 759 } 760 } 761 762 private class DatabaseHelper extends SQLiteOpenHelper { 763 Context mContext; 764 765 DatabaseHelper(Context context, String name) { 766 super(context, name, null, DATABASE_VERSION); 767 mContext = context; 768 } 769 770 @Override 771 public void onCreate(SQLiteDatabase db) { 772 Log.d(TAG, "Creating EmailProvider database"); 773 // Create all tables here; each class has its own method 774 createMessageTable(db); 775 createAttachmentTable(db); 776 createMailboxTable(db); 777 createHostAuthTable(db); 778 createAccountTable(db); 779 } 780 781 @Override 782 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 783 // For versions prior to 5, delete all data 784 // Versions >= 5 require that data be preserved! 785 if (oldVersion < 5) { 786 android.accounts.Account[] accounts = AccountManager.get(mContext) 787 .getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE); 788 for (android.accounts.Account account: accounts) { 789 AccountManager.get(mContext).removeAccount(account, null, null); 790 } 791 resetMessageTable(db, oldVersion, newVersion); 792 resetAttachmentTable(db, oldVersion, newVersion); 793 resetMailboxTable(db, oldVersion, newVersion); 794 resetHostAuthTable(db, oldVersion, newVersion); 795 resetAccountTable(db, oldVersion, newVersion); 796 return; 797 } 798 if (oldVersion == 5) { 799 // Message Tables: Add SyncColumns.SERVER_TIMESTAMP 800 try { 801 db.execSQL("alter table " + Message.TABLE_NAME 802 + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";"); 803 db.execSQL("alter table " + Message.UPDATED_TABLE_NAME 804 + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";"); 805 db.execSQL("alter table " + Message.DELETED_TABLE_NAME 806 + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";"); 807 } catch (SQLException e) { 808 // Shouldn't be needed unless we're debugging and interrupt the process 809 Log.w(TAG, "Exception upgrading EmailProvider.db from v5 to v6", e); 810 } 811 oldVersion = 6; 812 } 813 if (oldVersion == 6) { 814 // Use the newer mailbox_delete trigger 815 db.execSQL("drop trigger mailbox_delete;"); 816 db.execSQL(TRIGGER_MAILBOX_DELETE); 817 oldVersion = 7; 818 } 819 if (oldVersion == 7) { 820 // add the security (provisioning) column 821 try { 822 db.execSQL("alter table " + Account.TABLE_NAME 823 + " add column " + AccountColumns.SECURITY_FLAGS + " integer" + ";"); 824 } catch (SQLException e) { 825 // Shouldn't be needed unless we're debugging and interrupt the process 826 Log.w(TAG, "Exception upgrading EmailProvider.db from 7 to 8 " + e); 827 } 828 oldVersion = 8; 829 } 830 if (oldVersion == 8) { 831 // accounts: add security sync key & user signature columns 832 try { 833 db.execSQL("alter table " + Account.TABLE_NAME 834 + " add column " + AccountColumns.SECURITY_SYNC_KEY + " text" + ";"); 835 db.execSQL("alter table " + Account.TABLE_NAME 836 + " add column " + AccountColumns.SIGNATURE + " text" + ";"); 837 } catch (SQLException e) { 838 // Shouldn't be needed unless we're debugging and interrupt the process 839 Log.w(TAG, "Exception upgrading EmailProvider.db from 8 to 9 " + e); 840 } 841 oldVersion = 9; 842 } 843 if (oldVersion == 9) { 844 // Message: add meeting info column into Message tables 845 try { 846 db.execSQL("alter table " + Message.TABLE_NAME 847 + " add column " + MessageColumns.MEETING_INFO + " text" + ";"); 848 db.execSQL("alter table " + Message.UPDATED_TABLE_NAME 849 + " add column " + MessageColumns.MEETING_INFO + " text" + ";"); 850 db.execSQL("alter table " + Message.DELETED_TABLE_NAME 851 + " add column " + MessageColumns.MEETING_INFO + " text" + ";"); 852 } catch (SQLException e) { 853 // Shouldn't be needed unless we're debugging and interrupt the process 854 Log.w(TAG, "Exception upgrading EmailProvider.db from 9 to 10 " + e); 855 } 856 oldVersion = 10; 857 } 858 if (oldVersion == 10) { 859 // Attachment: add content and flags columns 860 try { 861 db.execSQL("alter table " + Attachment.TABLE_NAME 862 + " add column " + AttachmentColumns.CONTENT + " text" + ";"); 863 db.execSQL("alter table " + Attachment.TABLE_NAME 864 + " add column " + AttachmentColumns.FLAGS + " integer" + ";"); 865 } catch (SQLException e) { 866 // Shouldn't be needed unless we're debugging and interrupt the process 867 Log.w(TAG, "Exception upgrading EmailProvider.db from 10 to 11 " + e); 868 } 869 oldVersion = 11; 870 } 871 if (oldVersion == 11) { 872 // Attachment: add content_bytes 873 try { 874 db.execSQL("alter table " + Attachment.TABLE_NAME 875 + " add column " + AttachmentColumns.CONTENT_BYTES + " blob" + ";"); 876 } catch (SQLException e) { 877 // Shouldn't be needed unless we're debugging and interrupt the process 878 Log.w(TAG, "Exception upgrading EmailProvider.db from 11 to 12 " + e); 879 } 880 oldVersion = 12; 881 } 882 if (oldVersion == 12) { 883 try { 884 db.execSQL("alter table " + Mailbox.TABLE_NAME 885 + " add column " + Mailbox.MESSAGE_COUNT 886 +" integer not null default 0" + ";"); 887 recalculateMessageCount(db); 888 } catch (SQLException e) { 889 // Shouldn't be needed unless we're debugging and interrupt the process 890 Log.w(TAG, "Exception upgrading EmailProvider.db from 12 to 13 " + e); 891 } 892 oldVersion = 13; 893 } 894 if (oldVersion == 13) { 895 try { 896 db.execSQL("alter table " + Message.TABLE_NAME 897 + " add column " + Message.SNIPPET 898 +" text" + ";"); 899 } catch (SQLException e) { 900 // Shouldn't be needed unless we're debugging and interrupt the process 901 Log.w(TAG, "Exception upgrading EmailProvider.db from 13 to 14 " + e); 902 } 903 oldVersion = 14; 904 } 905 if (oldVersion == 14) { 906 try { 907 db.execSQL("alter table " + Message.DELETED_TABLE_NAME 908 + " add column " + Message.SNIPPET +" text" + ";"); 909 db.execSQL("alter table " + Message.UPDATED_TABLE_NAME 910 + " add column " + Message.SNIPPET +" text" + ";"); 911 } catch (SQLException e) { 912 // Shouldn't be needed unless we're debugging and interrupt the process 913 Log.w(TAG, "Exception upgrading EmailProvider.db from 14 to 15 " + e); 914 } 915 oldVersion = 15; 916 } 917 if (oldVersion == 15) { 918 try { 919 db.execSQL("alter table " + Attachment.TABLE_NAME 920 + " add column " + Attachment.ACCOUNT_KEY +" integer" + ";"); 921 // Update all existing attachments to add the accountKey data 922 db.execSQL("update " + Attachment.TABLE_NAME + " set " + 923 Attachment.ACCOUNT_KEY + "= (SELECT " + Message.TABLE_NAME + "." + 924 Message.ACCOUNT_KEY + " from " + Message.TABLE_NAME + " where " + 925 Message.TABLE_NAME + "." + Message.RECORD_ID + " = " + 926 Attachment.TABLE_NAME + "." + Attachment.MESSAGE_KEY + ")"); 927 } catch (SQLException e) { 928 // Shouldn't be needed unless we're debugging and interrupt the process 929 Log.w(TAG, "Exception upgrading EmailProvider.db from 15 to 16 " + e); 930 } 931 oldVersion = 16; 932 } 933 } 934 935 @Override 936 public void onOpen(SQLiteDatabase db) { 937 } 938 } 939 940 @Override 941 public int delete(Uri uri, String selection, String[] selectionArgs) { 942 final int match = findMatch(uri, "delete"); 943 Context context = getContext(); 944 // Pick the correct database for this operation 945 // If we're in a transaction already (which would happen during applyBatch), then the 946 // body database is already attached to the email database and any attempt to use the 947 // body database directly will result in a SQLiteException (the database is locked) 948 SQLiteDatabase db = getDatabase(context); 949 int table = match >> BASE_SHIFT; 950 String id = "0"; 951 boolean messageDeletion = false; 952 ContentResolver resolver = context.getContentResolver(); 953 954 ContentCache cache = CONTENT_CACHES[table]; 955 String tableName = TABLE_NAMES[table]; 956 int result = -1; 957 958 try { 959 switch (match) { 960 // These are cases in which one or more Messages might get deleted, either by 961 // cascade or explicitly 962 case MAILBOX_ID: 963 case MAILBOX: 964 case ACCOUNT_ID: 965 case ACCOUNT: 966 case MESSAGE: 967 case SYNCED_MESSAGE_ID: 968 case MESSAGE_ID: 969 // Handle lost Body records here, since this cannot be done in a trigger 970 // The process is: 971 // 1) Begin a transaction, ensuring that both databases are affected atomically 972 // 2) Do the requested deletion, with cascading deletions handled in triggers 973 // 3) End the transaction, committing all changes atomically 974 // 975 // Bodies are auto-deleted here; Attachments are auto-deleted via trigger 976 messageDeletion = true; 977 db.beginTransaction(); 978 resolver.notifyChange(Message.NOTIFIER_URI, null); 979 break; 980 } 981 switch (match) { 982 case BODY_ID: 983 case DELETED_MESSAGE_ID: 984 case SYNCED_MESSAGE_ID: 985 case MESSAGE_ID: 986 case UPDATED_MESSAGE_ID: 987 case ATTACHMENT_ID: 988 case MAILBOX_ID: 989 case ACCOUNT_ID: 990 case HOSTAUTH_ID: 991 id = uri.getPathSegments().get(1); 992 if (match == SYNCED_MESSAGE_ID) { 993 // For synced messages, first copy the old message to the deleted table and 994 // delete it from the updated table (in case it was updated first) 995 // Note that this is all within a transaction, for atomicity 996 db.execSQL(DELETED_MESSAGE_INSERT + id); 997 db.execSQL(UPDATED_MESSAGE_DELETE + id); 998 } 999 if (cache != null) { 1000 cache.lock(id); 1001 } 1002 try { 1003 result = db.delete(tableName, whereWithId(id, selection), selectionArgs); 1004 if (cache != null) { 1005 switch(match) { 1006 case ACCOUNT_ID: 1007 // Account deletion will clear all of the caches, as HostAuth's, 1008 // Mailboxes, and Messages will be deleted in the process 1009 sCacheMailbox.invalidate("Delete", uri, selection); 1010 sCacheHostAuth.invalidate("Delete", uri, selection); 1011 //$FALL-THROUGH$ 1012 case MAILBOX_ID: 1013 // Mailbox deletion will clear the Message cache 1014 sCacheMessage.invalidate("Delete", uri, selection); 1015 //$FALL-THROUGH$ 1016 case SYNCED_MESSAGE_ID: 1017 case MESSAGE_ID: 1018 case HOSTAUTH_ID: 1019 cache.invalidate("Delete", uri, selection); 1020 break; 1021 } 1022 } 1023 } finally { 1024 if (cache != null) { 1025 cache.unlock(id); 1026 } 1027 } 1028 break; 1029 case ATTACHMENTS_MESSAGE_ID: 1030 // All attachments for the given message 1031 id = uri.getPathSegments().get(2); 1032 result = db.delete(tableName, 1033 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs); 1034 break; 1035 1036 case BODY: 1037 case MESSAGE: 1038 case DELETED_MESSAGE: 1039 case UPDATED_MESSAGE: 1040 case ATTACHMENT: 1041 case MAILBOX: 1042 case ACCOUNT: 1043 case HOSTAUTH: 1044 switch(match) { 1045 // See the comments above for deletion of ACCOUNT_ID, etc 1046 case ACCOUNT: 1047 sCacheMailbox.invalidate("Delete", uri, selection); 1048 sCacheHostAuth.invalidate("Delete", uri, selection); 1049 //$FALL-THROUGH$ 1050 case MAILBOX: 1051 sCacheMessage.invalidate("Delete", uri, selection); 1052 //$FALL-THROUGH$ 1053 case MESSAGE: 1054 case HOSTAUTH: 1055 cache.invalidate("Delete", uri, selection); 1056 break; 1057 } 1058 result = db.delete(tableName, selection, selectionArgs); 1059 break; 1060 1061 default: 1062 throw new IllegalArgumentException("Unknown URI " + uri); 1063 } 1064 if (messageDeletion) { 1065 if (match == MESSAGE_ID) { 1066 // Delete the Body record associated with the deleted message 1067 db.execSQL(DELETE_BODY + id); 1068 } else { 1069 // Delete any orphaned Body records 1070 db.execSQL(DELETE_ORPHAN_BODIES); 1071 } 1072 db.setTransactionSuccessful(); 1073 } 1074 } catch (SQLiteException e) { 1075 checkDatabases(); 1076 throw e; 1077 } finally { 1078 if (messageDeletion) { 1079 db.endTransaction(); 1080 } 1081 } 1082 1083 // Notify all existing cursors. 1084 resolver.notifyChange(EmailContent.CONTENT_URI, null); 1085 return result; 1086 } 1087 1088 @Override 1089 // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) 1090 public String getType(Uri uri) { 1091 int match = findMatch(uri, "getType"); 1092 switch (match) { 1093 case BODY_ID: 1094 return "vnd.android.cursor.item/email-body"; 1095 case BODY: 1096 return "vnd.android.cursor.dir/email-body"; 1097 case UPDATED_MESSAGE_ID: 1098 case MESSAGE_ID: 1099 // NOTE: According to the framework folks, we're supposed to invent mime types as 1100 // a way of passing information to drag & drop recipients. 1101 // If there's a mailboxId parameter in the url, we respond with a mime type that 1102 // has -n appended, where n is the mailboxId of the message. The drag & drop code 1103 // uses this information to know not to allow dragging the item to its own mailbox 1104 String mimeType = EMAIL_MESSAGE_MIME_TYPE; 1105 String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID); 1106 if (mailboxId != null) { 1107 mimeType += "-" + mailboxId; 1108 } 1109 return mimeType; 1110 case UPDATED_MESSAGE: 1111 case MESSAGE: 1112 return "vnd.android.cursor.dir/email-message"; 1113 case MAILBOX: 1114 return "vnd.android.cursor.dir/email-mailbox"; 1115 case MAILBOX_ID: 1116 return "vnd.android.cursor.item/email-mailbox"; 1117 case ACCOUNT: 1118 return "vnd.android.cursor.dir/email-account"; 1119 case ACCOUNT_ID: 1120 return "vnd.android.cursor.item/email-account"; 1121 case ATTACHMENTS_MESSAGE_ID: 1122 case ATTACHMENT: 1123 return "vnd.android.cursor.dir/email-attachment"; 1124 case ATTACHMENT_ID: 1125 return EMAIL_ATTACHMENT_MIME_TYPE; 1126 case HOSTAUTH: 1127 return "vnd.android.cursor.dir/email-hostauth"; 1128 case HOSTAUTH_ID: 1129 return "vnd.android.cursor.item/email-hostauth"; 1130 default: 1131 throw new IllegalArgumentException("Unknown URI " + uri); 1132 } 1133 } 1134 1135 @Override 1136 public Uri insert(Uri uri, ContentValues values) { 1137 int match = findMatch(uri, "insert"); 1138 Context context = getContext(); 1139 ContentResolver resolver = context.getContentResolver(); 1140 1141 // See the comment at delete(), above 1142 SQLiteDatabase db = getDatabase(context); 1143 int table = match >> BASE_SHIFT; 1144 long id; 1145 1146 // We do NOT allow setting of unreadCount/messageCount via the provider 1147 // These columns are maintained via triggers 1148 if (match == MAILBOX_ID || match == MAILBOX) { 1149 values.put(MailboxColumns.UNREAD_COUNT, 0); 1150 values.put(MailboxColumns.MESSAGE_COUNT, 0); 1151 } 1152 1153 Uri resultUri = null; 1154 1155 try { 1156 switch (match) { 1157 case MESSAGE: 1158 resolver.notifyChange(Message.NOTIFIER_URI, null); 1159 //$FALL-THROUGH$ 1160 case UPDATED_MESSAGE: 1161 case DELETED_MESSAGE: 1162 case BODY: 1163 case ATTACHMENT: 1164 case MAILBOX: 1165 case ACCOUNT: 1166 case HOSTAUTH: 1167 id = db.insert(TABLE_NAMES[table], "foo", values); 1168 resultUri = ContentUris.withAppendedId(uri, id); 1169 // Clients shouldn't normally be adding rows to these tables, as they are 1170 // maintained by triggers. However, we need to be able to do this for unit 1171 // testing, so we allow the insert and then throw the same exception that we 1172 // would if this weren't allowed. 1173 if (match == UPDATED_MESSAGE || match == DELETED_MESSAGE) { 1174 throw new IllegalArgumentException("Unknown URL " + uri); 1175 } 1176 if (match == ATTACHMENT) { 1177 int flags = 0; 1178 if (values.containsKey(Attachment.FLAGS)) { 1179 flags = values.getAsInteger(Attachment.FLAGS); 1180 } 1181 // Report all new attachments to the download service 1182 AttachmentDownloadService.attachmentChanged(id, flags); 1183 } 1184 break; 1185 case MAILBOX_ID: 1186 // This implies adding a message to a mailbox 1187 // Hmm, a problem here is that we can't link the account as well, so it must be 1188 // already in the values... 1189 id = Long.parseLong(uri.getPathSegments().get(1)); 1190 values.put(MessageColumns.MAILBOX_KEY, id); 1191 return insert(Message.CONTENT_URI, values); // Recurse 1192 case MESSAGE_ID: 1193 // This implies adding an attachment to a message. 1194 id = Long.parseLong(uri.getPathSegments().get(1)); 1195 values.put(AttachmentColumns.MESSAGE_KEY, id); 1196 return insert(Attachment.CONTENT_URI, values); // Recurse 1197 case ACCOUNT_ID: 1198 // This implies adding a mailbox to an account. 1199 id = Long.parseLong(uri.getPathSegments().get(1)); 1200 values.put(MailboxColumns.ACCOUNT_KEY, id); 1201 return insert(Mailbox.CONTENT_URI, values); // Recurse 1202 case ATTACHMENTS_MESSAGE_ID: 1203 id = db.insert(TABLE_NAMES[table], "foo", values); 1204 resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, id); 1205 break; 1206 default: 1207 throw new IllegalArgumentException("Unknown URL " + uri); 1208 } 1209 } catch (SQLiteException e) { 1210 checkDatabases(); 1211 throw e; 1212 } 1213 1214 // Notify all existing cursors. 1215 resolver.notifyChange(EmailContent.CONTENT_URI, null); 1216 return resultUri; 1217 } 1218 1219 @Override 1220 public boolean onCreate() { 1221 checkDatabases(); 1222 return false; 1223 } 1224 1225 /** 1226 * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must 1227 * always be in sync (i.e. there are two database or NO databases). This code will delete 1228 * any "orphan" database, so that both will be created together. Note that an "orphan" database 1229 * will exist after either of the individual databases is deleted due to data corruption. 1230 */ 1231 public void checkDatabases() { 1232 // Uncache the databases 1233 if (mDatabase != null) { 1234 mDatabase = null; 1235 } 1236 if (mBodyDatabase != null) { 1237 mBodyDatabase = null; 1238 } 1239 // Look for orphans, and delete as necessary; these must always be in sync 1240 File databaseFile = getContext().getDatabasePath(DATABASE_NAME); 1241 File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME); 1242 1243 // TODO Make sure attachments are deleted 1244 if (databaseFile.exists() && !bodyFile.exists()) { 1245 Log.w(TAG, "Deleting orphaned EmailProvider database..."); 1246 databaseFile.delete(); 1247 } else if (bodyFile.exists() && !databaseFile.exists()) { 1248 Log.w(TAG, "Deleting orphaned EmailProviderBody database..."); 1249 bodyFile.delete(); 1250 } 1251 } 1252 1253 @Override 1254 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1255 String sortOrder) { 1256 long time = 0L; 1257 if (Email.DEBUG) { 1258 time = System.nanoTime(); 1259 } 1260 Cursor c = null; 1261 int match = findMatch(uri, "query"); 1262 Context context = getContext(); 1263 // See the comment at delete(), above 1264 SQLiteDatabase db = getDatabase(context); 1265 int table = match >> BASE_SHIFT; 1266 String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT); 1267 String id; 1268 1269 // Find the cache for this query's table (if any) 1270 ContentCache cache = null; 1271 String tableName = TABLE_NAMES[table]; 1272 // We can only use the cache if there's no selection 1273 if (selection == null) { 1274 cache = CONTENT_CACHES[table]; 1275 } 1276 if (cache == null) { 1277 ContentCache.notCacheable(uri, selection); 1278 } 1279 1280 try { 1281 switch (match) { 1282 case BODY: 1283 case MESSAGE: 1284 case UPDATED_MESSAGE: 1285 case DELETED_MESSAGE: 1286 case ATTACHMENT: 1287 case MAILBOX: 1288 case ACCOUNT: 1289 case HOSTAUTH: 1290 c = db.query(tableName, projection, 1291 selection, selectionArgs, null, null, sortOrder, limit); 1292 break; 1293 case BODY_ID: 1294 case MESSAGE_ID: 1295 case DELETED_MESSAGE_ID: 1296 case UPDATED_MESSAGE_ID: 1297 case ATTACHMENT_ID: 1298 case MAILBOX_ID: 1299 case ACCOUNT_ID: 1300 case HOSTAUTH_ID: 1301 id = uri.getPathSegments().get(1); 1302 if (cache != null) { 1303 c = cache.getCachedCursor(id, projection); 1304 } 1305 if (c == null) { 1306 CacheToken token = null; 1307 if (cache != null) { 1308 token = cache.getCacheToken(id); 1309 } 1310 c = db.query(tableName, projection, whereWithId(id, selection), 1311 selectionArgs, null, null, sortOrder, limit); 1312 if (cache != null) { 1313 c = cache.putCursor(c, id, projection, token); 1314 } 1315 } 1316 break; 1317 case ATTACHMENTS_MESSAGE_ID: 1318 // All attachments for the given message 1319 id = uri.getPathSegments().get(2); 1320 c = db.query(Attachment.TABLE_NAME, projection, 1321 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), 1322 selectionArgs, null, null, sortOrder, limit); 1323 break; 1324 default: 1325 throw new IllegalArgumentException("Unknown URI " + uri); 1326 } 1327 } catch (SQLiteException e) { 1328 checkDatabases(); 1329 throw e; 1330 } catch (RuntimeException e) { 1331 checkDatabases(); 1332 e.printStackTrace(); 1333 throw e; 1334 } finally { 1335 if (cache != null && Email.DEBUG) { 1336 cache.recordQueryTime(c, System.nanoTime() - time); 1337 } 1338 } 1339 1340 if ((c != null) && !isTemporary()) { 1341 c.setNotificationUri(getContext().getContentResolver(), uri); 1342 } 1343 return c; 1344 } 1345 1346 private String whereWithId(String id, String selection) { 1347 StringBuilder sb = new StringBuilder(256); 1348 sb.append("_id="); 1349 sb.append(id); 1350 if (selection != null) { 1351 sb.append(" AND ("); 1352 sb.append(selection); 1353 sb.append(')'); 1354 } 1355 return sb.toString(); 1356 } 1357 1358 /** 1359 * Combine a locally-generated selection with a user-provided selection 1360 * 1361 * This introduces risk that the local selection might insert incorrect chars 1362 * into the SQL, so use caution. 1363 * 1364 * @param where locally-generated selection, must not be null 1365 * @param selection user-provided selection, may be null 1366 * @return a single selection string 1367 */ 1368 private String whereWith(String where, String selection) { 1369 if (selection == null) { 1370 return where; 1371 } 1372 StringBuilder sb = new StringBuilder(where); 1373 sb.append(" AND ("); 1374 sb.append(selection); 1375 sb.append(')'); 1376 1377 return sb.toString(); 1378 } 1379 1380 @Override 1381 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1382 // Handle this special case the fastest possible way 1383 if (uri == INTEGRITY_CHECK_URI) { 1384 checkDatabases(); 1385 return 0; 1386 } 1387 1388 // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID) 1389 Uri notificationUri = EmailContent.CONTENT_URI; 1390 1391 int match = findMatch(uri, "update"); 1392 Context context = getContext(); 1393 ContentResolver resolver = context.getContentResolver(); 1394 // See the comment at delete(), above 1395 SQLiteDatabase db = getDatabase(context); 1396 int table = match >> BASE_SHIFT; 1397 int result; 1398 1399 // We do NOT allow setting of unreadCount/messageCount via the provider 1400 // These columns are maintained via triggers 1401 if (match == MAILBOX_ID || match == MAILBOX) { 1402 values.remove(MailboxColumns.UNREAD_COUNT); 1403 values.remove(MailboxColumns.MESSAGE_COUNT); 1404 } 1405 1406 ContentCache cache = CONTENT_CACHES[table]; 1407 String tableName = TABLE_NAMES[table]; 1408 String id; 1409 1410 try { 1411 switch (match) { 1412 case MAILBOX_ID_ADD_TO_FIELD: 1413 case ACCOUNT_ID_ADD_TO_FIELD: 1414 id = uri.getPathSegments().get(1); 1415 String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME); 1416 Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME); 1417 if (field == null || add == null) { 1418 throw new IllegalArgumentException("No field/add specified " + uri); 1419 } 1420 ContentValues actualValues = new ContentValues(); 1421 if (cache != null) { 1422 cache.lock(id); 1423 } 1424 try { 1425 db.beginTransaction(); 1426 try { 1427 Cursor c = db.query(tableName, 1428 new String[] {EmailContent.RECORD_ID, field}, 1429 whereWithId(id, selection), 1430 selectionArgs, null, null, null); 1431 try { 1432 result = 0; 1433 String[] bind = new String[1]; 1434 if (c.moveToNext()) { 1435 bind[0] = c.getString(0); // _id 1436 long value = c.getLong(1) + add; 1437 actualValues.put(field, value); 1438 result = db.update(tableName, actualValues, ID_EQUALS, bind); 1439 } 1440 db.setTransactionSuccessful(); 1441 } finally { 1442 c.close(); 1443 } 1444 } finally { 1445 db.endTransaction(); 1446 } 1447 } finally { 1448 if (cache != null) { 1449 cache.unlock(id, actualValues); 1450 } 1451 } 1452 break; 1453 case SYNCED_MESSAGE_ID: 1454 resolver.notifyChange(Message.NOTIFIER_URI, null); 1455 //$FALL-THROUGH$ 1456 case UPDATED_MESSAGE_ID: 1457 case MESSAGE_ID: 1458 case BODY_ID: 1459 case ATTACHMENT_ID: 1460 case MAILBOX_ID: 1461 case ACCOUNT_ID: 1462 case HOSTAUTH_ID: 1463 id = uri.getPathSegments().get(1); 1464 if (cache != null) { 1465 cache.lock(id); 1466 } 1467 try { 1468 if (match == SYNCED_MESSAGE_ID) { 1469 // For synced messages, first copy the old message to the updated table 1470 // Note the insert or ignore semantics, guaranteeing that only the first 1471 // update will be reflected in the updated message table; therefore this 1472 // row will always have the "original" data 1473 db.execSQL(UPDATED_MESSAGE_INSERT + id); 1474 } else if (match == MESSAGE_ID) { 1475 db.execSQL(UPDATED_MESSAGE_DELETE + id); 1476 } 1477 result = db.update(tableName, values, whereWithId(id, selection), 1478 selectionArgs); 1479 } catch (SQLiteException e) { 1480 // Null out values (so they aren't cached) and re-throw 1481 values = null; 1482 throw e; 1483 } finally { 1484 if (cache != null) { 1485 cache.unlock(id, values); 1486 } 1487 } 1488 if (match == ATTACHMENT_ID) { 1489 if (values.containsKey(Attachment.FLAGS)) { 1490 int flags = values.getAsInteger(Attachment.FLAGS); 1491 AttachmentDownloadService.attachmentChanged( 1492 Integer.parseInt(id), flags); 1493 } 1494 } 1495 break; 1496 case BODY: 1497 case MESSAGE: 1498 case UPDATED_MESSAGE: 1499 case ATTACHMENT: 1500 case MAILBOX: 1501 case ACCOUNT: 1502 case HOSTAUTH: 1503 switch(match) { 1504 case MESSAGE: 1505 case ACCOUNT: 1506 case MAILBOX: 1507 case HOSTAUTH: 1508 // If we're doing some generic update, the whole cache needs to be 1509 // invalidated. This case should be quite rare 1510 cache.invalidate("Update", uri, selection); 1511 break; 1512 } 1513 result = db.update(tableName, values, selection, selectionArgs); 1514 break; 1515 case ACCOUNT_RESET_NEW_COUNT_ID: 1516 id = uri.getPathSegments().get(1); 1517 if (cache != null) { 1518 cache.lock(id); 1519 } 1520 try { 1521 result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT, 1522 whereWithId(id, selection), selectionArgs); 1523 } finally { 1524 if (cache != null) { 1525 cache.unlock(id, values); 1526 } 1527 } 1528 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1529 break; 1530 case ACCOUNT_RESET_NEW_COUNT: 1531 result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT, 1532 selection, selectionArgs); 1533 // Affects all accounts. Just invalidate all account cache. 1534 cache.invalidate("Reset all new counts", null, null); 1535 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1536 break; 1537 default: 1538 throw new IllegalArgumentException("Unknown URI " + uri); 1539 } 1540 } catch (SQLiteException e) { 1541 checkDatabases(); 1542 throw e; 1543 } 1544 1545 resolver.notifyChange(notificationUri, null); 1546 return result; 1547 } 1548 1549 /* (non-Javadoc) 1550 * @see android.content.ContentProvider#applyBatch(android.content.ContentProviderOperation) 1551 */ 1552 @Override 1553 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1554 throws OperationApplicationException { 1555 Context context = getContext(); 1556 SQLiteDatabase db = getDatabase(context); 1557 db.beginTransaction(); 1558 try { 1559 ContentProviderResult[] results = super.applyBatch(operations); 1560 db.setTransactionSuccessful(); 1561 return results; 1562 } finally { 1563 db.endTransaction(); 1564 } 1565 } 1566 1567 /** 1568 * Count the number of messages in each mailbox, and update the message count column. 1569 */ 1570 /* package */ static void recalculateMessageCount(SQLiteDatabase db) { 1571 db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT + 1572 "= (select count(*) from " + Message.TABLE_NAME + 1573 " where " + Message.MAILBOX_KEY + " = " + 1574 Mailbox.TABLE_NAME + "." + EmailContent.RECORD_ID + ")"); 1575 } 1576} 1577