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