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