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