EmailProvider.java revision 6c36b4c613499655316d8c910e3c6bfb08a0d896
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 /** {@inheritDoc} */ 639 @Override 640 public void shutdown() { 641 if (mDatabase != null) { 642 mDatabase.close(); 643 mDatabase = null; 644 } 645 if (mBodyDatabase != null) { 646 mBodyDatabase.close(); 647 mBodyDatabase = null; 648 } 649 } 650 651 /*package*/ static void deleteOrphans(SQLiteDatabase database, String tableName) { 652 if (database != null) { 653 // We'll look at all of the items in the table; there won't be many typically 654 Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); 655 // Usually, there will be nothing in these tables, so make a quick check 656 try { 657 if (c.getCount() == 0) return; 658 ArrayList<Long> foundMailboxes = new ArrayList<Long>(); 659 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>(); 660 ArrayList<Long> deleteList = new ArrayList<Long>(); 661 String[] bindArray = new String[1]; 662 while (c.moveToNext()) { 663 // Get the mailbox key and see if we've already found this mailbox 664 // If so, we're fine 665 long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY); 666 // If we already know this mailbox doesn't exist, mark the message for deletion 667 if (notFoundMailboxes.contains(mailboxId)) { 668 deleteList.add(c.getLong(ORPHANS_ID)); 669 // If we don't know about this mailbox, we'll try to find it 670 } else if (!foundMailboxes.contains(mailboxId)) { 671 bindArray[0] = Long.toString(mailboxId); 672 Cursor boxCursor = database.query(Mailbox.TABLE_NAME, 673 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null); 674 try { 675 // If it exists, we'll add it to the "found" mailboxes 676 if (boxCursor.moveToFirst()) { 677 foundMailboxes.add(mailboxId); 678 // Otherwise, we'll add to "not found" and mark the message for deletion 679 } else { 680 notFoundMailboxes.add(mailboxId); 681 deleteList.add(c.getLong(ORPHANS_ID)); 682 } 683 } finally { 684 boxCursor.close(); 685 } 686 } 687 } 688 // Now, delete the orphan messages 689 for (long messageId: deleteList) { 690 bindArray[0] = Long.toString(messageId); 691 database.delete(tableName, WHERE_ID, bindArray); 692 } 693 } finally { 694 c.close(); 695 } 696 } 697 } 698 699 private class BodyDatabaseHelper extends SQLiteOpenHelper { 700 BodyDatabaseHelper(Context context, String name) { 701 super(context, name, null, BODY_DATABASE_VERSION); 702 } 703 704 @Override 705 public void onCreate(SQLiteDatabase db) { 706 Log.d(TAG, "Creating EmailProviderBody database"); 707 createBodyTable(db); 708 } 709 710 @Override 711 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 712 upgradeBodyTable(db, oldVersion, newVersion); 713 } 714 715 @Override 716 public void onOpen(SQLiteDatabase db) { 717 } 718 } 719 720 private class DatabaseHelper extends SQLiteOpenHelper { 721 Context mContext; 722 723 DatabaseHelper(Context context, String name) { 724 super(context, name, null, DATABASE_VERSION); 725 mContext = context; 726 } 727 728 @Override 729 public void onCreate(SQLiteDatabase db) { 730 Log.d(TAG, "Creating EmailProvider database"); 731 // Create all tables here; each class has its own method 732 createMessageTable(db); 733 createAttachmentTable(db); 734 createMailboxTable(db); 735 createHostAuthTable(db); 736 createAccountTable(db); 737 } 738 739 @Override 740 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 741 // For versions prior to 5, delete all data 742 // Versions >= 5 require that data be preserved! 743 if (oldVersion < 5) { 744 android.accounts.Account[] accounts = AccountManager.get(mContext) 745 .getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE); 746 for (android.accounts.Account account: accounts) { 747 AccountManager.get(mContext).removeAccount(account, null, null); 748 } 749 resetMessageTable(db, oldVersion, newVersion); 750 resetAttachmentTable(db, oldVersion, newVersion); 751 resetMailboxTable(db, oldVersion, newVersion); 752 resetHostAuthTable(db, oldVersion, newVersion); 753 resetAccountTable(db, oldVersion, newVersion); 754 return; 755 } 756 if (oldVersion == 5) { 757 // Message Tables: Add SyncColumns.SERVER_TIMESTAMP 758 try { 759 db.execSQL("alter table " + Message.TABLE_NAME 760 + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";"); 761 db.execSQL("alter table " + Message.UPDATED_TABLE_NAME 762 + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";"); 763 db.execSQL("alter table " + Message.DELETED_TABLE_NAME 764 + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";"); 765 } catch (SQLException e) { 766 // Shouldn't be needed unless we're debugging and interrupt the process 767 Log.w(TAG, "Exception upgrading EmailProvider.db from v5 to v6", e); 768 } 769 oldVersion = 6; 770 } 771 if (oldVersion == 6) { 772 // Use the newer mailbox_delete trigger 773 db.execSQL("drop trigger mailbox_delete;"); 774 db.execSQL(TRIGGER_MAILBOX_DELETE); 775 oldVersion = 7; 776 } 777 if (oldVersion == 7) { 778 // add the security (provisioning) column 779 try { 780 db.execSQL("alter table " + Account.TABLE_NAME 781 + " add column " + AccountColumns.SECURITY_FLAGS + " integer" + ";"); 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 7 to 8 " + e); 785 } 786 oldVersion = 8; 787 } 788 if (oldVersion == 8) { 789 // accounts: add security sync key & user signature columns 790 try { 791 db.execSQL("alter table " + Account.TABLE_NAME 792 + " add column " + AccountColumns.SECURITY_SYNC_KEY + " text" + ";"); 793 db.execSQL("alter table " + Account.TABLE_NAME 794 + " add column " + AccountColumns.SIGNATURE + " text" + ";"); 795 } catch (SQLException e) { 796 // Shouldn't be needed unless we're debugging and interrupt the process 797 Log.w(TAG, "Exception upgrading EmailProvider.db from 8 to 9 " + e); 798 } 799 oldVersion = 9; 800 } 801 if (oldVersion == 9) { 802 // Message: add meeting info column into Message tables 803 try { 804 db.execSQL("alter table " + Message.TABLE_NAME 805 + " add column " + MessageColumns.MEETING_INFO + " text" + ";"); 806 db.execSQL("alter table " + Message.UPDATED_TABLE_NAME 807 + " add column " + MessageColumns.MEETING_INFO + " text" + ";"); 808 db.execSQL("alter table " + Message.DELETED_TABLE_NAME 809 + " add column " + MessageColumns.MEETING_INFO + " text" + ";"); 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 9 to 10 " + e); 813 } 814 oldVersion = 10; 815 } 816 if (oldVersion == 10) { 817 // Attachment: add content and flags columns 818 try { 819 db.execSQL("alter table " + Attachment.TABLE_NAME 820 + " add column " + AttachmentColumns.CONTENT + " text" + ";"); 821 db.execSQL("alter table " + Attachment.TABLE_NAME 822 + " add column " + AttachmentColumns.FLAGS + " integer" + ";"); 823 } catch (SQLException e) { 824 // Shouldn't be needed unless we're debugging and interrupt the process 825 Log.w(TAG, "Exception upgrading EmailProvider.db from 10 to 11 " + e); 826 } 827 oldVersion = 11; 828 } 829 if (oldVersion == 11) { 830 // Attachment: add content_bytes 831 try { 832 db.execSQL("alter table " + Attachment.TABLE_NAME 833 + " add column " + AttachmentColumns.CONTENT_BYTES + " blob" + ";"); 834 } catch (SQLException e) { 835 // Shouldn't be needed unless we're debugging and interrupt the process 836 Log.w(TAG, "Exception upgrading EmailProvider.db from 11 to 12 " + e); 837 } 838 oldVersion = 12; 839 } 840 if (oldVersion == 12) { 841 try { 842 db.execSQL("alter table " + Mailbox.TABLE_NAME 843 + " add column " + Mailbox.MESSAGE_COUNT 844 +" integer not null default 0" + ";"); 845 recalculateMessageCount(db); 846 } catch (SQLException e) { 847 // Shouldn't be needed unless we're debugging and interrupt the process 848 Log.w(TAG, "Exception upgrading EmailProvider.db from 12 to 13 " + e); 849 } 850 oldVersion = 13; 851 } 852 } 853 854 @Override 855 public void onOpen(SQLiteDatabase db) { 856 } 857 } 858 859 @Override 860 public int delete(Uri uri, String selection, String[] selectionArgs) { 861 final int match = sURIMatcher.match(uri); 862 Context context = getContext(); 863 // Pick the correct database for this operation 864 // If we're in a transaction already (which would happen during applyBatch), then the 865 // body database is already attached to the email database and any attempt to use the 866 // body database directly will result in a SQLiteException (the database is locked) 867 SQLiteDatabase db = getDatabase(context); 868 int table = match >> BASE_SHIFT; 869 String id = "0"; 870 boolean messageDeletion = false; 871 872 if (Email.LOGD) { 873 Log.v(TAG, "EmailProvider.delete: uri=" + uri + ", match is " + match); 874 } 875 876 int result = -1; 877 878 try { 879 switch (match) { 880 // These are cases in which one or more Messages might get deleted, either by 881 // cascade or explicitly 882 case MAILBOX_ID: 883 case MAILBOX: 884 case ACCOUNT_ID: 885 case ACCOUNT: 886 case MESSAGE: 887 case SYNCED_MESSAGE_ID: 888 case MESSAGE_ID: 889 // Handle lost Body records here, since this cannot be done in a trigger 890 // The process is: 891 // 1) Begin a transaction, ensuring that both databases are affected atomically 892 // 2) Do the requested deletion, with cascading deletions handled in triggers 893 // 3) End the transaction, committing all changes atomically 894 // 895 // Bodies are auto-deleted here; Attachments are auto-deleted via trigger 896 897 messageDeletion = true; 898 db.beginTransaction(); 899 break; 900 } 901 switch (match) { 902 case BODY_ID: 903 case DELETED_MESSAGE_ID: 904 case SYNCED_MESSAGE_ID: 905 case MESSAGE_ID: 906 case UPDATED_MESSAGE_ID: 907 case ATTACHMENT_ID: 908 case MAILBOX_ID: 909 case ACCOUNT_ID: 910 case HOSTAUTH_ID: 911 id = uri.getPathSegments().get(1); 912 if (match == SYNCED_MESSAGE_ID) { 913 // For synced messages, first copy the old message to the deleted table and 914 // delete it from the updated table (in case it was updated first) 915 // Note that this is all within a transaction, for atomicity 916 db.execSQL(DELETED_MESSAGE_INSERT + id); 917 db.execSQL(UPDATED_MESSAGE_DELETE + id); 918 } 919 result = db.delete(TABLE_NAMES[table], whereWithId(id, selection), 920 selectionArgs); 921 break; 922 case ATTACHMENTS_MESSAGE_ID: 923 // All attachments for the given message 924 id = uri.getPathSegments().get(2); 925 result = db.delete(TABLE_NAMES[table], 926 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs); 927 break; 928 929 case BODY: 930 case MESSAGE: 931 case DELETED_MESSAGE: 932 case UPDATED_MESSAGE: 933 case ATTACHMENT: 934 case MAILBOX: 935 case ACCOUNT: 936 case HOSTAUTH: 937 result = db.delete(TABLE_NAMES[table], selection, selectionArgs); 938 break; 939 940 default: 941 throw new IllegalArgumentException("Unknown URI " + uri); 942 } 943 if (messageDeletion) { 944 if (match == MESSAGE_ID) { 945 // Delete the Body record associated with the deleted message 946 db.execSQL(DELETE_BODY + id); 947 } else { 948 // Delete any orphaned Body records 949 db.execSQL(DELETE_ORPHAN_BODIES); 950 } 951 db.setTransactionSuccessful(); 952 } 953 } catch (SQLiteException e) { 954 checkDatabases(); 955 throw e; 956 } finally { 957 if (messageDeletion) { 958 db.endTransaction(); 959 } 960 } 961 getContext().getContentResolver().notifyChange(uri, null); 962 return result; 963 } 964 965 @Override 966 // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) 967 public String getType(Uri uri) { 968 int match = sURIMatcher.match(uri); 969 switch (match) { 970 case BODY_ID: 971 return "vnd.android.cursor.item/email-body"; 972 case BODY: 973 return "vnd.android.cursor.dir/email-message"; 974 case UPDATED_MESSAGE_ID: 975 case MESSAGE_ID: 976 return "vnd.android.cursor.item/email-message"; 977 case MAILBOX_MESSAGES: 978 case UPDATED_MESSAGE: 979 case MESSAGE: 980 return "vnd.android.cursor.dir/email-message"; 981 case ACCOUNT_MAILBOXES: 982 case MAILBOX: 983 return "vnd.android.cursor.dir/email-mailbox"; 984 case MAILBOX_ID: 985 return "vnd.android.cursor.item/email-mailbox"; 986 case ACCOUNT: 987 return "vnd.android.cursor.dir/email-account"; 988 case ACCOUNT_ID: 989 return "vnd.android.cursor.item/email-account"; 990 case ATTACHMENTS_MESSAGE_ID: 991 case ATTACHMENT: 992 return "vnd.android.cursor.dir/email-attachment"; 993 case ATTACHMENT_ID: 994 return EMAIL_ATTACHMENT_MIME_TYPE; 995 case HOSTAUTH: 996 return "vnd.android.cursor.dir/email-hostauth"; 997 case HOSTAUTH_ID: 998 return "vnd.android.cursor.item/email-hostauth"; 999 default: 1000 throw new IllegalArgumentException("Unknown URI " + uri); 1001 } 1002 } 1003 1004 @Override 1005 public Uri insert(Uri uri, ContentValues values) { 1006 int match = sURIMatcher.match(uri); 1007 Context context = getContext(); 1008 // See the comment at delete(), above 1009 SQLiteDatabase db = getDatabase(context); 1010 int table = match >> BASE_SHIFT; 1011 long id; 1012 1013 if (Email.LOGD) { 1014 Log.v(TAG, "EmailProvider.insert: uri=" + uri + ", match is " + match); 1015 } 1016 1017 Uri resultUri = null; 1018 1019 try { 1020 switch (match) { 1021 case UPDATED_MESSAGE: 1022 case DELETED_MESSAGE: 1023 case BODY: 1024 case MESSAGE: 1025 case ATTACHMENT: 1026 case MAILBOX: 1027 case ACCOUNT: 1028 case HOSTAUTH: 1029 id = db.insert(TABLE_NAMES[table], "foo", values); 1030 resultUri = ContentUris.withAppendedId(uri, id); 1031 // Clients shouldn't normally be adding rows to these tables, as they are 1032 // maintained by triggers. However, we need to be able to do this for unit 1033 // testing, so we allow the insert and then throw the same exception that we 1034 // would if this weren't allowed. 1035 if (match == UPDATED_MESSAGE || match == DELETED_MESSAGE) { 1036 throw new IllegalArgumentException("Unknown URL " + uri); 1037 } 1038 if (match == ATTACHMENT) { 1039 if (values.containsKey(Attachment.FLAGS)) { 1040 int flags = values.getAsInteger(Attachment.FLAGS); 1041 AttachmentDownloadService.attachmentChanged(id, flags); 1042 } 1043 } 1044 break; 1045 case MAILBOX_ID: 1046 // This implies adding a message to a mailbox 1047 // Hmm, a problem here is that we can't link the account as well, so it must be 1048 // already in the values... 1049 id = Long.parseLong(uri.getPathSegments().get(1)); 1050 values.put(MessageColumns.MAILBOX_KEY, id); 1051 resultUri = insert(Message.CONTENT_URI, values); 1052 break; 1053 case MESSAGE_ID: 1054 // This implies adding an attachment to a message. 1055 id = Long.parseLong(uri.getPathSegments().get(1)); 1056 values.put(AttachmentColumns.MESSAGE_KEY, id); 1057 resultUri = insert(Attachment.CONTENT_URI, values); 1058 break; 1059 case ACCOUNT_ID: 1060 // This implies adding a mailbox to an account. 1061 id = Long.parseLong(uri.getPathSegments().get(1)); 1062 values.put(MailboxColumns.ACCOUNT_KEY, id); 1063 resultUri = insert(Mailbox.CONTENT_URI, values); 1064 break; 1065 case ATTACHMENTS_MESSAGE_ID: 1066 id = db.insert(TABLE_NAMES[table], "foo", values); 1067 resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, id); 1068 break; 1069 default: 1070 throw new IllegalArgumentException("Unknown URL " + uri); 1071 } 1072 } catch (SQLiteException e) { 1073 checkDatabases(); 1074 throw e; 1075 } 1076 1077 // Notify with the base uri, not the new uri (nobody is watching a new record) 1078 getContext().getContentResolver().notifyChange(uri, null); 1079 return resultUri; 1080 } 1081 1082 @Override 1083 public boolean onCreate() { 1084 checkDatabases(); 1085 return false; 1086 } 1087 1088 /** 1089 * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must 1090 * always be in sync (i.e. there are two database or NO databases). This code will delete 1091 * any "orphan" database, so that both will be created together. Note that an "orphan" database 1092 * will exist after either of the individual databases is deleted due to data corruption. 1093 */ 1094 public void checkDatabases() { 1095 // Uncache the databases 1096 if (mDatabase != null) { 1097 mDatabase = null; 1098 } 1099 if (mBodyDatabase != null) { 1100 mBodyDatabase = null; 1101 } 1102 // Look for orphans, and delete as necessary; these must always be in sync 1103 File databaseFile = getContext().getDatabasePath(DATABASE_NAME); 1104 File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME); 1105 1106 // TODO Make sure attachments are deleted 1107 if (databaseFile.exists() && !bodyFile.exists()) { 1108 Log.w(TAG, "Deleting orphaned EmailProvider database..."); 1109 databaseFile.delete(); 1110 } else if (bodyFile.exists() && !databaseFile.exists()) { 1111 Log.w(TAG, "Deleting orphaned EmailProviderBody database..."); 1112 bodyFile.delete(); 1113 } 1114 } 1115 1116 @Override 1117 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1118 String sortOrder) { 1119 Cursor c = null; 1120 Uri notificationUri = EmailContent.CONTENT_URI; 1121 int match = sURIMatcher.match(uri); 1122 Context context = getContext(); 1123 // See the comment at delete(), above 1124 SQLiteDatabase db = getDatabase(context); 1125 int table = match >> BASE_SHIFT; 1126 String id; 1127 1128 if (Email.LOGD) { 1129 Log.v(TAG, "EmailProvider.query: uri=" + uri + ", match is " + match); 1130 } 1131 1132 try { 1133 switch (match) { 1134 case BODY: 1135 case MESSAGE: 1136 case UPDATED_MESSAGE: 1137 case DELETED_MESSAGE: 1138 case ATTACHMENT: 1139 case MAILBOX: 1140 case ACCOUNT: 1141 case HOSTAUTH: 1142 c = db.query(TABLE_NAMES[table], projection, 1143 selection, selectionArgs, null, null, sortOrder); 1144 break; 1145 case BODY_ID: 1146 case MESSAGE_ID: 1147 case DELETED_MESSAGE_ID: 1148 case UPDATED_MESSAGE_ID: 1149 case ATTACHMENT_ID: 1150 case MAILBOX_ID: 1151 case ACCOUNT_ID: 1152 case HOSTAUTH_ID: 1153 id = uri.getPathSegments().get(1); 1154 c = db.query(TABLE_NAMES[table], projection, 1155 whereWithId(id, selection), selectionArgs, null, null, sortOrder); 1156 break; 1157 case ATTACHMENTS_MESSAGE_ID: 1158 // All attachments for the given message 1159 id = uri.getPathSegments().get(2); 1160 c = db.query(Attachment.TABLE_NAME, projection, 1161 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), 1162 selectionArgs, null, null, sortOrder); 1163 break; 1164 default: 1165 throw new IllegalArgumentException("Unknown URI " + uri); 1166 } 1167 } catch (SQLiteException e) { 1168 checkDatabases(); 1169 throw e; 1170 } 1171 1172 if ((c != null) && !isTemporary()) { 1173 c.setNotificationUri(getContext().getContentResolver(), notificationUri); 1174 } 1175 return c; 1176 } 1177 1178 private String whereWithId(String id, String selection) { 1179 StringBuilder sb = new StringBuilder(256); 1180 sb.append("_id="); 1181 sb.append(id); 1182 if (selection != null) { 1183 sb.append(" AND ("); 1184 sb.append(selection); 1185 sb.append(')'); 1186 } 1187 return sb.toString(); 1188 } 1189 1190 /** 1191 * Combine a locally-generated selection with a user-provided selection 1192 * 1193 * This introduces risk that the local selection might insert incorrect chars 1194 * into the SQL, so use caution. 1195 * 1196 * @param where locally-generated selection, must not be null 1197 * @param selection user-provided selection, may be null 1198 * @return a single selection string 1199 */ 1200 private String whereWith(String where, String selection) { 1201 if (selection == null) { 1202 return where; 1203 } 1204 StringBuilder sb = new StringBuilder(where); 1205 sb.append(" AND ("); 1206 sb.append(selection); 1207 sb.append(')'); 1208 1209 return sb.toString(); 1210 } 1211 1212 @Override 1213 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1214 int match = sURIMatcher.match(uri); 1215 Context context = getContext(); 1216 // See the comment at delete(), above 1217 SQLiteDatabase db = getDatabase(context); 1218 int table = match >> BASE_SHIFT; 1219 int result; 1220 1221 if (Email.LOGD) { 1222 Log.v(TAG, "EmailProvider.update: uri=" + uri + ", match is " + match); 1223 } 1224 1225 // We do NOT allow setting of unreadCount via the provider 1226 // This column is maintained via triggers 1227 if (match == MAILBOX_ID || match == MAILBOX) { 1228 values.remove(MailboxColumns.UNREAD_COUNT); 1229 } 1230 1231 // Handle this special case the fastest possible way 1232 if (uri == INTEGRITY_CHECK_URI) { 1233 checkDatabases(); 1234 return 0; 1235 } 1236 1237 String id; 1238 try { 1239 switch (match) { 1240 case MAILBOX_ID_ADD_TO_FIELD: 1241 case ACCOUNT_ID_ADD_TO_FIELD: 1242 db.beginTransaction(); 1243 id = uri.getPathSegments().get(1); 1244 String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME); 1245 Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME); 1246 if (field == null || add == null) { 1247 throw new IllegalArgumentException("No field/add specified " + uri); 1248 } 1249 Cursor c = db.query(TABLE_NAMES[table], 1250 new String[] {EmailContent.RECORD_ID, field}, 1251 whereWithId(id, selection), 1252 selectionArgs, null, null, null); 1253 try { 1254 result = 0; 1255 ContentValues cv = new ContentValues(); 1256 String[] bind = new String[1]; 1257 while (c.moveToNext()) { 1258 bind[0] = c.getString(0); 1259 long value = c.getLong(1) + add; 1260 cv.put(field, value); 1261 result = db.update(TABLE_NAMES[table], cv, ID_EQUALS, bind); 1262 } 1263 } finally { 1264 c.close(); 1265 } 1266 db.setTransactionSuccessful(); 1267 db.endTransaction(); 1268 break; 1269 case BODY_ID: 1270 case MESSAGE_ID: 1271 case SYNCED_MESSAGE_ID: 1272 case UPDATED_MESSAGE_ID: 1273 case ATTACHMENT_ID: 1274 case MAILBOX_ID: 1275 case ACCOUNT_ID: 1276 case HOSTAUTH_ID: 1277 id = uri.getPathSegments().get(1); 1278 if (match == SYNCED_MESSAGE_ID) { 1279 // For synced messages, first copy the old message to the updated table 1280 // Note the insert or ignore semantics, guaranteeing that only the first 1281 // update will be reflected in the updated message table; therefore this row 1282 // will always have the "original" data 1283 db.execSQL(UPDATED_MESSAGE_INSERT + id); 1284 } else if (match == MESSAGE_ID) { 1285 db.execSQL(UPDATED_MESSAGE_DELETE + id); 1286 } 1287 result = db.update(TABLE_NAMES[table], values, whereWithId(id, selection), 1288 selectionArgs); 1289 if (match == ATTACHMENT_ID) { 1290 if (values.containsKey(Attachment.FLAGS)) { 1291 int flags = values.getAsInteger(Attachment.FLAGS); 1292 AttachmentDownloadService.attachmentChanged( 1293 Integer.parseInt(id), flags); 1294 } 1295 } 1296 break; 1297 case BODY: 1298 case MESSAGE: 1299 case UPDATED_MESSAGE: 1300 case ATTACHMENT: 1301 case MAILBOX: 1302 case ACCOUNT: 1303 case HOSTAUTH: 1304 result = db.update(TABLE_NAMES[table], values, selection, selectionArgs); 1305 break; 1306 default: 1307 throw new IllegalArgumentException("Unknown URI " + uri); 1308 } 1309 } catch (SQLiteException e) { 1310 checkDatabases(); 1311 throw e; 1312 } 1313 1314 context.getContentResolver().notifyChange(uri, null); 1315 return result; 1316 } 1317 1318 /* (non-Javadoc) 1319 * @see android.content.ContentProvider#applyBatch(android.content.ContentProviderOperation) 1320 */ 1321 @Override 1322 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1323 throws OperationApplicationException { 1324 Context context = getContext(); 1325 SQLiteDatabase db = getDatabase(context); 1326 db.beginTransaction(); 1327 try { 1328 ContentProviderResult[] results = super.applyBatch(operations); 1329 db.setTransactionSuccessful(); 1330 return results; 1331 } finally { 1332 db.endTransaction(); 1333 } 1334 } 1335 1336 /** 1337 * Count the number of messages in each mailbox, and update the message count column. 1338 */ 1339 /* package */ static void recalculateMessageCount(SQLiteDatabase db) { 1340 db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.MESSAGE_COUNT + 1341 "= (select count(*) from " + Message.TABLE_NAME + 1342 " where " + Message.MAILBOX_KEY + " = " + 1343 Mailbox.TABLE_NAME + "." + EmailContent.RECORD_ID + ")"); 1344 } 1345} 1346