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