EmailProvider.java revision e7e1ca432e1aace5b7f11a3f1684a4abc503e6b2
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.exchange.Eas; 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.SQLiteOpenHelper; 48import android.net.Uri; 49import android.util.Log; 50 51import java.util.ArrayList; 52 53public class EmailProvider extends ContentProvider { 54 55 private static final String TAG = "EmailProvider"; 56 57 static final String DATABASE_NAME = "EmailProvider.db"; 58 static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; 59 60 // Any changes to the database format *must* include update-in-place code. 61 // Original version: 3 62 // Version 4: Database wipe required; changing AccountManager interface w/Exchange 63 public static final int DATABASE_VERSION = 4; 64 65 // Any changes to the database format *must* include update-in-place code. 66 // Original version: 2 67 // Version 3: Add "sourceKey" column 68 // Version 4: Database wipe required; changing AccountManager interface w/Exchange 69 public static final int BODY_DATABASE_VERSION = 4; 70 71 public static final String EMAIL_AUTHORITY = "com.android.email.provider"; 72 73 private static final int ACCOUNT_BASE = 0; 74 private static final int ACCOUNT = ACCOUNT_BASE; 75 private static final int ACCOUNT_MAILBOXES = ACCOUNT_BASE + 1; 76 private static final int ACCOUNT_ID = ACCOUNT_BASE + 2; 77 private static final int ACCOUNT_ID_ADD_TO_FIELD = ACCOUNT_BASE + 3; 78 79 private static final int MAILBOX_BASE = 0x1000; 80 private static final int MAILBOX = MAILBOX_BASE; 81 private static final int MAILBOX_MESSAGES = MAILBOX_BASE + 1; 82 private static final int MAILBOX_ID = MAILBOX_BASE + 2; 83 private static final int MAILBOX_ID_ADD_TO_FIELD = MAILBOX_BASE + 3; 84 85 private static final int MESSAGE_BASE = 0x2000; 86 private static final int MESSAGE = MESSAGE_BASE; 87 private static final int MESSAGE_ID = MESSAGE_BASE + 1; 88 private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2; 89 90 private static final int ATTACHMENT_BASE = 0x3000; 91 private static final int ATTACHMENT = ATTACHMENT_BASE; 92 private static final int ATTACHMENT_CONTENT = ATTACHMENT_BASE + 1; 93 private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 2; 94 private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 3; 95 96 private static final int HOSTAUTH_BASE = 0x4000; 97 private static final int HOSTAUTH = HOSTAUTH_BASE; 98 private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1; 99 100 private static final int UPDATED_MESSAGE_BASE = 0x5000; 101 private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE; 102 private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1; 103 104 private static final int DELETED_MESSAGE_BASE = 0x6000; 105 private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE; 106 private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1; 107 private static final int DELETED_MESSAGE_MAILBOX = DELETED_MESSAGE_BASE + 2; 108 109 // MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS 110 private static final int LAST_EMAIL_PROVIDER_DB_BASE = DELETED_MESSAGE_BASE; 111 112 // DO NOT CHANGE BODY_BASE!! 113 private static final int BODY_BASE = LAST_EMAIL_PROVIDER_DB_BASE + 0x1000; 114 private static final int BODY = BODY_BASE; 115 private static final int BODY_ID = BODY_BASE + 1; 116 private static final int BODY_MESSAGE_ID = BODY_BASE + 2; 117 private static final int BODY_HTML = BODY_BASE + 3; 118 private static final int BODY_TEXT = BODY_BASE + 4; 119 120 121 private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc. 122 123 private static final String[] TABLE_NAMES = { 124 EmailContent.Account.TABLE_NAME, 125 EmailContent.Mailbox.TABLE_NAME, 126 EmailContent.Message.TABLE_NAME, 127 EmailContent.Attachment.TABLE_NAME, 128 EmailContent.HostAuth.TABLE_NAME, 129 EmailContent.Message.UPDATED_TABLE_NAME, 130 EmailContent.Message.DELETED_TABLE_NAME, 131 EmailContent.Body.TABLE_NAME 132 }; 133 134 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 135 136 /** 137 * Let's only generate these SQL strings once, as they are used frequently 138 * Note that this isn't relevant for table creation strings, since they are used only once 139 */ 140 private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " + 141 Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 142 EmailContent.RECORD_ID + '='; 143 144 private static final String UPDATED_MESSAGE_DELETE = "delete from " + 145 Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '='; 146 147 private static final String DELETED_MESSAGE_INSERT = "insert or replace into " + 148 Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 149 EmailContent.RECORD_ID + '='; 150 151 private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME + 152 " where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY + 153 " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " + 154 Message.TABLE_NAME + ')'; 155 156 private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME + 157 " where " + BodyColumns.MESSAGE_KEY + '='; 158 159 private static final String ID_EQUALS = EmailContent.RECORD_ID + "=?"; 160 161 static { 162 // Email URI matching table 163 UriMatcher matcher = sURIMatcher; 164 165 // All accounts 166 matcher.addURI(EMAIL_AUTHORITY, "account", ACCOUNT); 167 // A specific account 168 // insert into this URI causes a mailbox to be added to the account 169 matcher.addURI(EMAIL_AUTHORITY, "account/#", ACCOUNT_ID); 170 // The mailboxes in a specific account 171 matcher.addURI(EMAIL_AUTHORITY, "account/#/mailbox", ACCOUNT_MAILBOXES); 172 173 // All mailboxes 174 matcher.addURI(EMAIL_AUTHORITY, "mailbox", MAILBOX); 175 // A specific mailbox 176 // insert into this URI causes a message to be added to the mailbox 177 // ** NOTE For now, the accountKey must be set manually in the values! 178 matcher.addURI(EMAIL_AUTHORITY, "mailbox/#", MAILBOX_ID); 179 // The messages in a specific mailbox 180 matcher.addURI(EMAIL_AUTHORITY, "mailbox/#/message", MAILBOX_MESSAGES); 181 182 // All messages 183 matcher.addURI(EMAIL_AUTHORITY, "message", MESSAGE); 184 // A specific message 185 // insert into this URI causes an attachment to be added to the message 186 matcher.addURI(EMAIL_AUTHORITY, "message/#", MESSAGE_ID); 187 188 // A specific attachment 189 matcher.addURI(EMAIL_AUTHORITY, "attachment", ATTACHMENT); 190 // A specific attachment (the header information) 191 matcher.addURI(EMAIL_AUTHORITY, "attachment/#", ATTACHMENT_ID); 192 // The content for a specific attachment 193 // NOT IMPLEMENTED 194 matcher.addURI(EMAIL_AUTHORITY, "attachment/content/*", ATTACHMENT_CONTENT); 195 // The attachments of a specific message (query only) (insert & delete TBD) 196 matcher.addURI(EMAIL_AUTHORITY, "attachment/message/#", ATTACHMENTS_MESSAGE_ID); 197 198 // All mail bodies 199 matcher.addURI(EMAIL_AUTHORITY, "body", BODY); 200 // A specific mail body 201 matcher.addURI(EMAIL_AUTHORITY, "body/#", BODY_ID); 202 // The body for a specific message 203 matcher.addURI(EMAIL_AUTHORITY, "body/message/#", BODY_MESSAGE_ID); 204 // The HTML part of a specific mail body 205 matcher.addURI(EMAIL_AUTHORITY, "body/#/html", BODY_HTML); 206 // The plain text part of a specific mail body 207 matcher.addURI(EMAIL_AUTHORITY, "body/#/text", BODY_TEXT); 208 209 // All hostauth records 210 matcher.addURI(EMAIL_AUTHORITY, "hostauth", HOSTAUTH); 211 // A specific hostauth 212 matcher.addURI(EMAIL_AUTHORITY, "hostauth/#", HOSTAUTH_ID); 213 214 // Atomically a constant value to a particular field of a mailbox/account 215 matcher.addURI(EMAIL_AUTHORITY, "mailboxIdAddToField/#", MAILBOX_ID_ADD_TO_FIELD); 216 matcher.addURI(EMAIL_AUTHORITY, "accountIdAddToField/#", ACCOUNT_ID_ADD_TO_FIELD); 217 218 /** 219 * THIS URI HAS SPECIAL SEMANTICS 220 * ITS USE IS INTENDED FOR THE UI APPLICATION TO MARK CHANGES THAT NEED TO BE SYNCED BACK 221 * TO A SERVER VIA A SYNC ADAPTER 222 */ 223 matcher.addURI(EMAIL_AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); 224 225 /** 226 * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY 227 * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI 228 * BY THE UI APPLICATION 229 */ 230 // All deleted messages 231 matcher.addURI(EMAIL_AUTHORITY, "deletedMessage", DELETED_MESSAGE); 232 // A specific deleted message 233 matcher.addURI(EMAIL_AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID); 234 // All deleted messages from a specific mailbox 235 // NOT IMPLEMENTED; do we need this as a convenience? 236 matcher.addURI(EMAIL_AUTHORITY, "deletedMessage/mailbox/#", DELETED_MESSAGE_MAILBOX); 237 238 // All updated messages 239 matcher.addURI(EMAIL_AUTHORITY, "updatedMessage", UPDATED_MESSAGE); 240 // A specific updated message 241 matcher.addURI(EMAIL_AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID); 242 } 243 244 /* 245 * Internal helper method for index creation. 246 * Example: 247 * "create index message_" + MessageColumns.FLAG_READ 248 * + " on " + Message.TABLE_NAME + " (" + MessageColumns.FLAG_READ + ");" 249 */ 250 /* package */ 251 static String createIndex(String tableName, String columnName) { 252 return "create index " + tableName.toLowerCase() + '_' + columnName 253 + " on " + tableName + " (" + columnName + ");"; 254 } 255 256 static void createMessageTable(SQLiteDatabase db) { 257 String messageColumns = MessageColumns.DISPLAY_NAME + " text, " 258 + MessageColumns.TIMESTAMP + " integer, " 259 + MessageColumns.SUBJECT + " text, " 260 + MessageColumns.FLAG_READ + " integer, " 261 + MessageColumns.FLAG_LOADED + " integer, " 262 + MessageColumns.FLAG_FAVORITE + " integer, " 263 + MessageColumns.FLAG_ATTACHMENT + " integer, " 264 + MessageColumns.FLAGS + " integer, " 265 + MessageColumns.CLIENT_ID + " integer, " 266 + MessageColumns.MESSAGE_ID + " text, " 267 + MessageColumns.MAILBOX_KEY + " integer, " 268 + MessageColumns.ACCOUNT_KEY + " integer, " 269 + MessageColumns.FROM_LIST + " text, " 270 + MessageColumns.TO_LIST + " text, " 271 + MessageColumns.CC_LIST + " text, " 272 + MessageColumns.BCC_LIST + " text, " 273 + MessageColumns.REPLY_TO_LIST + " text" 274 + ");"; 275 276 // This String and the following String MUST have the same columns, except for the type 277 // of those columns! 278 String createString = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 279 + SyncColumns.SERVER_ID + " integer, " 280 + messageColumns; 281 282 // For the updated and deleted tables, the id is assigned, but we do want to keep track 283 // of the ORDER of updates using an autoincrement primary key. We use the DATA column 284 // at this point; it has no other function 285 String altCreateString = " (" + EmailContent.RECORD_ID + " integer unique, " 286 + SyncColumns.SERVER_ID + " integer, " 287 + messageColumns; 288 289 // The three tables have the same schema 290 db.execSQL("create table " + Message.TABLE_NAME + createString); 291 db.execSQL("create table " + Message.UPDATED_TABLE_NAME + altCreateString); 292 db.execSQL("create table " + Message.DELETED_TABLE_NAME + altCreateString); 293 294 String indexColumns[] = { 295 MessageColumns.TIMESTAMP, 296 MessageColumns.FLAG_READ, 297 MessageColumns.FLAG_LOADED, 298 MessageColumns.MAILBOX_KEY, 299 SyncColumns.SERVER_ID 300 }; 301 302 for (String columnName : indexColumns) { 303 db.execSQL(createIndex(Message.TABLE_NAME, columnName)); 304 } 305 306 // Deleting a Message deletes all associated Attachments 307 // Deleting the associated Body cannot be done in a trigger, because the Body is stored 308 // in a separate database, and trigger cannot operate on attached databases. 309 db.execSQL("create trigger message_delete before delete on " + Message.TABLE_NAME + 310 " begin delete from " + Attachment.TABLE_NAME + 311 " where " + AttachmentColumns.MESSAGE_KEY + "=old." + EmailContent.RECORD_ID + 312 "; end"); 313 314 // Add triggers to keep unread count accurate per mailbox 315 316 // Insert a message; if flagRead is zero, add to the unread count of the message's mailbox 317 db.execSQL("create trigger unread_message_insert before insert on " + Message.TABLE_NAME + 318 " when NEW." + MessageColumns.FLAG_READ + "=0" + 319 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 320 '=' + MailboxColumns.UNREAD_COUNT + "+1" + 321 " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY + 322 "; end"); 323 324 // Delete a message; if flagRead is zero, decrement the unread count of the msg's mailbox 325 db.execSQL("create trigger unread_message_delete before delete on " + Message.TABLE_NAME + 326 " when OLD." + MessageColumns.FLAG_READ + "=0" + 327 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 328 '=' + MailboxColumns.UNREAD_COUNT + "-1" + 329 " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY + 330 "; end"); 331 332 // Change a message's mailbox 333 db.execSQL("create trigger unread_message_move before update of " + 334 MessageColumns.MAILBOX_KEY + " on " + Message.TABLE_NAME + 335 " when OLD." + MessageColumns.FLAG_READ + "=0" + 336 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 337 '=' + MailboxColumns.UNREAD_COUNT + "-1" + 338 " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY + 339 "; update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 340 '=' + MailboxColumns.UNREAD_COUNT + "+1" + 341 " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY + 342 "; end"); 343 344 // Change a message's read state 345 db.execSQL("create trigger unread_message_read before update of " + 346 MessageColumns.FLAG_READ + " on " + Message.TABLE_NAME + 347 " when OLD." + MessageColumns.FLAG_READ + "!=NEW." + MessageColumns.FLAG_READ + 348 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT + 349 '=' + MailboxColumns.UNREAD_COUNT + "+ case OLD." + MessageColumns.FLAG_READ + 350 " when 0 then -1 else 1 end" + 351 " where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY + 352 "; end"); 353 } 354 355 static void resetMessageTable(SQLiteDatabase db, int oldVersion, int newVersion) { 356 try { 357 db.execSQL("drop table " + Message.TABLE_NAME); 358 db.execSQL("drop table " + Message.UPDATED_TABLE_NAME); 359 db.execSQL("drop table " + Message.DELETED_TABLE_NAME); 360 } catch (SQLException e) { 361 } 362 createMessageTable(db); 363 } 364 365 static void createAccountTable(SQLiteDatabase db) { 366 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 367 + AccountColumns.DISPLAY_NAME + " text, " 368 + AccountColumns.EMAIL_ADDRESS + " text, " 369 + AccountColumns.SYNC_KEY + " text, " 370 + AccountColumns.SYNC_LOOKBACK + " integer, " 371 + AccountColumns.SYNC_INTERVAL + " text, " 372 + AccountColumns.HOST_AUTH_KEY_RECV + " integer, " 373 + AccountColumns.HOST_AUTH_KEY_SEND + " integer, " 374 + AccountColumns.FLAGS + " integer, " 375 + AccountColumns.IS_DEFAULT + " integer, " 376 + AccountColumns.COMPATIBILITY_UUID + " text, " 377 + AccountColumns.SENDER_NAME + " text, " 378 + AccountColumns.RINGTONE_URI + " text, " 379 + AccountColumns.PROTOCOL_VERSION + " text, " 380 + AccountColumns.NEW_MESSAGE_COUNT + " integer" 381 + ");"; 382 db.execSQL("create table " + Account.TABLE_NAME + s); 383 // Deleting an account deletes associated Mailboxes and HostAuth's 384 db.execSQL("create trigger account_delete before delete on " + Account.TABLE_NAME + 385 " begin delete from " + Mailbox.TABLE_NAME + 386 " where " + MailboxColumns.ACCOUNT_KEY + "=old." + EmailContent.RECORD_ID + 387 "; delete from " + HostAuth.TABLE_NAME + 388 " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_RECV + 389 "; delete from " + HostAuth.TABLE_NAME + 390 " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_SEND + 391 "; end"); 392 } 393 394 static void resetAccountTable(SQLiteDatabase db, int oldVersion, int newVersion) { 395 try { 396 db.execSQL("drop table " + Account.TABLE_NAME); 397 } catch (SQLException e) { 398 } 399 createAccountTable(db); 400 } 401 402 static void createHostAuthTable(SQLiteDatabase db) { 403 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 404 + HostAuthColumns.PROTOCOL + " text, " 405 + HostAuthColumns.ADDRESS + " text, " 406 + HostAuthColumns.PORT + " integer, " 407 + HostAuthColumns.FLAGS + " integer, " 408 + HostAuthColumns.LOGIN + " text, " 409 + HostAuthColumns.PASSWORD + " text, " 410 + HostAuthColumns.DOMAIN + " text, " 411 + HostAuthColumns.ACCOUNT_KEY + " integer" 412 + ");"; 413 db.execSQL("create table " + HostAuth.TABLE_NAME + s); 414 } 415 416 static void resetHostAuthTable(SQLiteDatabase db, int oldVersion, int newVersion) { 417 try { 418 db.execSQL("drop table " + HostAuth.TABLE_NAME); 419 } catch (SQLException e) { 420 } 421 createHostAuthTable(db); 422 } 423 424 static void createMailboxTable(SQLiteDatabase db) { 425 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 426 + MailboxColumns.DISPLAY_NAME + " text, " 427 + MailboxColumns.SERVER_ID + " text, " 428 + MailboxColumns.PARENT_SERVER_ID + " text, " 429 + MailboxColumns.ACCOUNT_KEY + " integer, " 430 + MailboxColumns.TYPE + " integer, " 431 + MailboxColumns.DELIMITER + " integer, " 432 + MailboxColumns.SYNC_KEY + " text, " 433 + MailboxColumns.SYNC_LOOKBACK + " integer, " 434 + MailboxColumns.SYNC_INTERVAL + " integer, " 435 + MailboxColumns.SYNC_TIME + " integer, " 436 + MailboxColumns.UNREAD_COUNT + " integer, " 437 + MailboxColumns.FLAG_VISIBLE + " integer, " 438 + MailboxColumns.FLAGS + " integer, " 439 + MailboxColumns.VISIBLE_LIMIT + " integer, " 440 + MailboxColumns.SYNC_STATUS + " text" 441 + ");"; 442 db.execSQL("create table " + Mailbox.TABLE_NAME + s); 443 db.execSQL("create index mailbox_" + MailboxColumns.SERVER_ID 444 + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.SERVER_ID + ")"); 445 db.execSQL("create index mailbox_" + MailboxColumns.ACCOUNT_KEY 446 + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.ACCOUNT_KEY + ")"); 447 // Deleting a Mailbox deletes associated Messages 448 db.execSQL("create trigger mailbox_delete before delete on " + Mailbox.TABLE_NAME + 449 " begin delete from " + Message.TABLE_NAME + 450 " where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID + 451 "; end"); 452 } 453 454 static void resetMailboxTable(SQLiteDatabase db, int oldVersion, int newVersion) { 455 try { 456 db.execSQL("drop table " + Mailbox.TABLE_NAME); 457 } catch (SQLException e) { 458 } 459 createMailboxTable(db); 460 } 461 462 static void createAttachmentTable(SQLiteDatabase db) { 463 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 464 + AttachmentColumns.FILENAME + " text, " 465 + AttachmentColumns.MIME_TYPE + " text, " 466 + AttachmentColumns.SIZE + " integer, " 467 + AttachmentColumns.CONTENT_ID + " text, " 468 + AttachmentColumns.CONTENT_URI + " text, " 469 + AttachmentColumns.MESSAGE_KEY + " integer, " 470 + AttachmentColumns.LOCATION + " text, " 471 + AttachmentColumns.ENCODING + " text" 472 + ");"; 473 db.execSQL("create table " + Attachment.TABLE_NAME + s); 474 db.execSQL(createIndex(Attachment.TABLE_NAME, AttachmentColumns.MESSAGE_KEY)); 475 } 476 477 static void resetAttachmentTable(SQLiteDatabase db, int oldVersion, int newVersion) { 478 try { 479 db.execSQL("drop table " + Attachment.TABLE_NAME); 480 } catch (SQLException e) { 481 } 482 createAttachmentTable(db); 483 } 484 485 static void createBodyTable(SQLiteDatabase db) { 486 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 487 + BodyColumns.MESSAGE_KEY + " integer, " 488 + BodyColumns.HTML_CONTENT + " text, " 489 + BodyColumns.TEXT_CONTENT + " text, " 490 + BodyColumns.HTML_REPLY + " text, " 491 + BodyColumns.TEXT_REPLY + " text, " 492 + BodyColumns.SOURCE_MESSAGE_KEY + " text" 493 + ");"; 494 db.execSQL("create table " + Body.TABLE_NAME + s); 495 db.execSQL(createIndex(Body.TABLE_NAME, BodyColumns.MESSAGE_KEY)); 496 } 497 498 static void upgradeBodyTable(SQLiteDatabase db, int oldVersion, int newVersion) { 499 if (oldVersion < 4) { 500 try { 501 db.execSQL("drop table " + Body.TABLE_NAME); 502 createBodyTable(db); 503 } catch (SQLException e) { 504 } 505 } 506 } 507 508 private final int mDatabaseVersion = DATABASE_VERSION; 509 private final int mBodyDatabaseVersion = BODY_DATABASE_VERSION; 510 511 private SQLiteDatabase mDatabase; 512 private SQLiteDatabase mBodyDatabase; 513 private boolean mInTransaction = false; 514 515 public synchronized SQLiteDatabase getDatabase(Context context) { 516 if (mDatabase != null) { 517 return mDatabase; 518 } 519 DatabaseHelper helper = new DatabaseHelper(context, DATABASE_NAME); 520 mDatabase = helper.getWritableDatabase(); 521 if (mDatabase != null) { 522 mDatabase.setLockingEnabled(true); 523 BodyDatabaseHelper bodyHelper = new BodyDatabaseHelper(context, BODY_DATABASE_NAME); 524 mBodyDatabase = bodyHelper.getWritableDatabase(); 525 if (mBodyDatabase != null) { 526 mBodyDatabase.setLockingEnabled(true); 527 String bodyFileName = mBodyDatabase.getPath(); 528 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); 529 } 530 } 531 return mDatabase; 532 } 533 534 private class BodyDatabaseHelper extends SQLiteOpenHelper { 535 BodyDatabaseHelper(Context context, String name) { 536 super(context, name, null, mBodyDatabaseVersion); 537 } 538 539 @Override 540 public void onCreate(SQLiteDatabase db) { 541 // Create all tables here; each class has its own method 542 createBodyTable(db); 543 } 544 545 @Override 546 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 547 upgradeBodyTable(db, oldVersion, newVersion); 548 } 549 550 @Override 551 public void onOpen(SQLiteDatabase db) { 552 } 553 } 554 555 private class DatabaseHelper extends SQLiteOpenHelper { 556 Context mContext; 557 558 DatabaseHelper(Context context, String name) { 559 super(context, name, null, mDatabaseVersion); 560 mContext = context; 561 } 562 563 @Override 564 public void onCreate(SQLiteDatabase db) { 565 // Create all tables here; each class has its own method 566 createMessageTable(db); 567 createAttachmentTable(db); 568 createMailboxTable(db); 569 createHostAuthTable(db); 570 createAccountTable(db); 571 } 572 573 @Override 574 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 575 // For versions prior to 4, delete all data 576 // Versions >= 4 require that data be preserved! 577 if (oldVersion < 4) { 578 android.accounts.Account[] accounts = 579 AccountManager.get(mContext).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE); 580 for (android.accounts.Account account: accounts) { 581 AccountManager.get(mContext).removeAccount(account, null, null); 582 } 583 resetMessageTable(db, oldVersion, newVersion); 584 resetAttachmentTable(db, oldVersion, newVersion); 585 resetMailboxTable(db, oldVersion, newVersion); 586 resetHostAuthTable(db, oldVersion, newVersion); 587 resetAccountTable(db, oldVersion, newVersion); 588 } 589 } 590 591 @Override 592 public void onOpen(SQLiteDatabase db) { 593 } 594 } 595 596 @Override 597 public int delete(Uri uri, String selection, String[] selectionArgs) { 598 final int match = sURIMatcher.match(uri); 599 Context context = getContext(); 600 // Pick the correct database for this operation 601 // If we're in a transaction already (which would happen during applyBatch), then the 602 // body database is already attached to the email database and any attempt to use the 603 // body database directly will result in a SQLiteException (the database is locked) 604 SQLiteDatabase db = getDatabase(context); 605 int table = match >> BASE_SHIFT; 606 String id = "0"; 607 boolean messageDeletion = false; 608 609 if (Email.LOGD) { 610 Log.v(TAG, "EmailProvider.delete: uri=" + uri + ", match is " + match); 611 } 612 613 int result = -1; 614 615 try { 616 switch (match) { 617 // These are cases in which one or more Messages might get deleted, either by 618 // cascade or explicitly 619 case MAILBOX_ID: 620 case MAILBOX: 621 case ACCOUNT_ID: 622 case ACCOUNT: 623 case MESSAGE: 624 case SYNCED_MESSAGE_ID: 625 case MESSAGE_ID: 626 // Handle lost Body records here, since this cannot be done in a trigger 627 // The process is: 628 // 1) Begin a transaction, ensuring that both databases are affected atomically 629 // 2) Do the requested deletion, with cascading deletions handled in triggers 630 // 3) End the transaction, committing all changes atomically 631 // 632 // Bodies are auto-deleted here; Attachments are auto-deleted via trigger 633 634 messageDeletion = true; 635 if (!mInTransaction) { 636 db.beginTransaction(); 637 } 638 break; 639 } 640 switch (match) { 641 case BODY_ID: 642 case DELETED_MESSAGE_ID: 643 case SYNCED_MESSAGE_ID: 644 case MESSAGE_ID: 645 case UPDATED_MESSAGE_ID: 646 case ATTACHMENT_ID: 647 case MAILBOX_ID: 648 case ACCOUNT_ID: 649 case HOSTAUTH_ID: 650 id = uri.getPathSegments().get(1); 651 if (match == SYNCED_MESSAGE_ID) { 652 // For synced messages, first copy the old message to the deleted table and 653 // delete it from the updated table (in case it was updated first) 654 // Note that this is all within a transaction, for atomicity 655 db.execSQL(DELETED_MESSAGE_INSERT + id); 656 db.execSQL(UPDATED_MESSAGE_DELETE + id); 657 } 658 result = db.delete(TABLE_NAMES[table], whereWithId(id, selection), 659 selectionArgs); 660 break; 661 case ATTACHMENTS_MESSAGE_ID: 662 // All attachments for the given message 663 id = uri.getPathSegments().get(2); 664 result = db.delete(TABLE_NAMES[table], 665 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs); 666 break; 667 668 case BODY: 669 case MESSAGE: 670 case DELETED_MESSAGE: 671 case UPDATED_MESSAGE: 672 case ATTACHMENT: 673 case MAILBOX: 674 case ACCOUNT: 675 case HOSTAUTH: 676 result = db.delete(TABLE_NAMES[table], selection, selectionArgs); 677 break; 678 679 default: 680 throw new IllegalArgumentException("Unknown URI " + uri); 681 } 682 if (messageDeletion) { 683 if (match == MESSAGE_ID) { 684 // Delete the Body record associated with the deleted message 685 db.execSQL(DELETE_BODY + id); 686 } else { 687 // Delete any orphaned Body records 688 db.execSQL(DELETE_ORPHAN_BODIES); 689 } 690 if (!mInTransaction) { 691 db.setTransactionSuccessful(); 692 } 693 } 694 } finally { 695 if (messageDeletion) { 696 if (!mInTransaction) { 697 db.endTransaction(); 698 } 699 } 700 } 701 getContext().getContentResolver().notifyChange(uri, null); 702 return result; 703 } 704 705 @Override 706 // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) 707 public String getType(Uri uri) { 708 int match = sURIMatcher.match(uri); 709 switch (match) { 710 case BODY_ID: 711 return "vnd.android.cursor.item/email-body"; 712 case BODY: 713 return "vnd.android.cursor.dir/email-message"; 714 case UPDATED_MESSAGE_ID: 715 case MESSAGE_ID: 716 return "vnd.android.cursor.item/email-message"; 717 case MAILBOX_MESSAGES: 718 case UPDATED_MESSAGE: 719 case MESSAGE: 720 return "vnd.android.cursor.dir/email-message"; 721 case ACCOUNT_MAILBOXES: 722 case MAILBOX: 723 return "vnd.android.cursor.dir/email-mailbox"; 724 case MAILBOX_ID: 725 return "vnd.android.cursor.item/email-mailbox"; 726 case ACCOUNT: 727 return "vnd.android.cursor.dir/email-account"; 728 case ACCOUNT_ID: 729 return "vnd.android.cursor.item/email-account"; 730 case ATTACHMENTS_MESSAGE_ID: 731 case ATTACHMENT: 732 return "vnd.android.cursor.dir/email-attachment"; 733 case ATTACHMENT_ID: 734 return "vnd.android.cursor.item/email-attachment"; 735 case HOSTAUTH: 736 return "vnd.android.cursor.dir/email-hostauth"; 737 case HOSTAUTH_ID: 738 return "vnd.android.cursor.item/email-hostauth"; 739 default: 740 throw new IllegalArgumentException("Unknown URI " + uri); 741 } 742 } 743 744 @Override 745 public Uri insert(Uri uri, ContentValues values) { 746 int match = sURIMatcher.match(uri); 747 Context context = getContext(); 748 // See the comment at delete(), above 749 SQLiteDatabase db = getDatabase(context); 750 int table = match >> BASE_SHIFT; 751 long id; 752 753 if (Email.LOGD) { 754 Log.v(TAG, "EmailProvider.insert: uri=" + uri + ", match is " + match); 755 } 756 757 Uri resultUri = null; 758 759 switch (match) { 760 case BODY: 761 case MESSAGE: 762 case ATTACHMENT: 763 case MAILBOX: 764 case ACCOUNT: 765 case HOSTAUTH: 766 id = db.insert(TABLE_NAMES[table], "foo", values); 767 resultUri = ContentUris.withAppendedId(uri, id); 768 break; 769 case MAILBOX_ID: 770 // This implies adding a message to a mailbox 771 // Hmm, one problem here is that we can't link the account as well, so it must be 772 // already in the values... 773 id = Long.parseLong(uri.getPathSegments().get(1)); 774 values.put(MessageColumns.MAILBOX_KEY, id); 775 resultUri = insert(Message.CONTENT_URI, values); 776 break; 777 case MESSAGE_ID: 778 // This implies adding an attachment to a message. 779 id = Long.parseLong(uri.getPathSegments().get(1)); 780 values.put(AttachmentColumns.MESSAGE_KEY, id); 781 resultUri = insert(Attachment.CONTENT_URI, values); 782 break; 783 case ACCOUNT_ID: 784 // This implies adding a mailbox to an account. 785 id = Long.parseLong(uri.getPathSegments().get(1)); 786 values.put(MailboxColumns.ACCOUNT_KEY, id); 787 resultUri = insert(Mailbox.CONTENT_URI, values); 788 break; 789 case ATTACHMENTS_MESSAGE_ID: 790 id = db.insert(TABLE_NAMES[table], "foo", values); 791 resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, id); 792 break; 793 default: 794 throw new IllegalArgumentException("Unknown URL " + uri); 795 } 796 797 // Notify with the base uri, not the new uri (nobody is watching a new record) 798 getContext().getContentResolver().notifyChange(uri, null); 799 return resultUri; 800 } 801 802 @Override 803 public boolean onCreate() { 804 // TODO Auto-generated method stub 805 return false; 806 } 807 808 @Override 809 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 810 String sortOrder) { 811 Cursor c = null; 812 Uri notificationUri = EmailContent.CONTENT_URI; 813 int match = sURIMatcher.match(uri); 814 Context context = getContext(); 815 // See the comment at delete(), above 816 SQLiteDatabase db = getDatabase(context); 817 int table = match >> BASE_SHIFT; 818 String id; 819 820 if (Email.LOGD) { 821 Log.v(TAG, "EmailProvider.query: uri=" + uri + ", match is " + match); 822 } 823 824 switch (match) { 825 case BODY: 826 case MESSAGE: 827 case UPDATED_MESSAGE: 828 case DELETED_MESSAGE: 829 case ATTACHMENT: 830 case MAILBOX: 831 case ACCOUNT: 832 case HOSTAUTH: 833 c = db.query(TABLE_NAMES[table], projection, 834 selection, selectionArgs, null, null, sortOrder); 835 break; 836 case BODY_ID: 837 case MESSAGE_ID: 838 case DELETED_MESSAGE_ID: 839 case UPDATED_MESSAGE_ID: 840 case ATTACHMENT_ID: 841 case MAILBOX_ID: 842 case ACCOUNT_ID: 843 case HOSTAUTH_ID: 844 id = uri.getPathSegments().get(1); 845 c = db.query(TABLE_NAMES[table], projection, 846 whereWithId(id, selection), selectionArgs, null, null, sortOrder); 847 break; 848 case ATTACHMENTS_MESSAGE_ID: 849 // All attachments for the given message 850 id = uri.getPathSegments().get(2); 851 c = db.query(Attachment.TABLE_NAME, projection, 852 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), 853 selectionArgs, null, null, sortOrder); 854 break; 855 default: 856 throw new IllegalArgumentException("Unknown URI " + uri); 857 } 858 859 if ((c != null) && !isTemporary()) { 860 c.setNotificationUri(getContext().getContentResolver(), notificationUri); 861 } 862 return c; 863 } 864 865 private String whereWithId(String id, String selection) { 866 StringBuilder sb = new StringBuilder(256); 867 sb.append("_id="); 868 sb.append(id); 869 if (selection != null) { 870 sb.append(" AND ("); 871 sb.append(selection); 872 sb.append(')'); 873 } 874 return sb.toString(); 875 } 876 877 /** 878 * Combine a locally-generated selection with a user-provided selection 879 * 880 * This introduces risk that the local selection might insert incorrect chars 881 * into the SQL, so use caution. 882 * 883 * @param where locally-generated selection, must not be null 884 * @param selection user-provided selection, may be null 885 * @return a single selection string 886 */ 887 private String whereWith(String where, String selection) { 888 if (selection == null) { 889 return where; 890 } 891 StringBuilder sb = new StringBuilder(where); 892 sb.append(" AND ("); 893 sb.append(selection); 894 sb.append(')'); 895 896 return sb.toString(); 897 } 898 899 @Override 900 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 901 int match = sURIMatcher.match(uri); 902 Context context = getContext(); 903 // See the comment at delete(), above 904 SQLiteDatabase db = getDatabase(context); 905 int table = match >> BASE_SHIFT; 906 int result; 907 908 if (Email.LOGD) { 909 Log.v(TAG, "EmailProvider.update: uri=" + uri + ", match is " + match); 910 } 911 912 // We do NOT allow setting of unreadCount via the provider 913 // This column is maintained via triggers 914 if (match == MAILBOX_ID || match == MAILBOX) { 915 values.remove(MailboxColumns.UNREAD_COUNT); 916 } 917 918 String id; 919 switch (match) { 920 case MAILBOX_ID_ADD_TO_FIELD: 921 case ACCOUNT_ID_ADD_TO_FIELD: 922 if (!mInTransaction) { 923 db.beginTransaction(); 924 } 925 id = uri.getPathSegments().get(1); 926 String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME); 927 Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME); 928 if (field == null || add == null) { 929 throw new IllegalArgumentException("No field/add specified " + uri); 930 } 931 Cursor c = db.query(TABLE_NAMES[table], 932 new String[] {EmailContent.RECORD_ID, field}, whereWithId(id, selection), 933 selectionArgs, null, null, null); 934 try { 935 result = 0; 936 ContentValues cv = new ContentValues(); 937 String[] bind = new String[1]; 938 while (c.moveToNext()) { 939 bind[0] = c.getString(0); 940 long value = c.getLong(1) + add; 941 cv.put(field, value); 942 result = db.update(TABLE_NAMES[table], cv, ID_EQUALS, bind); 943 } 944 } finally { 945 c.close(); 946 } 947 if (!mInTransaction) { 948 db.setTransactionSuccessful(); 949 db.endTransaction(); 950 } 951 break; 952 case BODY_ID: 953 case MESSAGE_ID: 954 case SYNCED_MESSAGE_ID: 955 case UPDATED_MESSAGE_ID: 956 case ATTACHMENT_ID: 957 case MAILBOX_ID: 958 case ACCOUNT_ID: 959 case HOSTAUTH_ID: 960 id = uri.getPathSegments().get(1); 961 if (match == SYNCED_MESSAGE_ID) { 962 // For synced messages, first copy the old message to the updated table 963 // Note the insert or ignore semantics, guaranteeing that only the first 964 // update will be reflected in the updated message table; therefore this row 965 // will always have the "original" data 966 db.execSQL(UPDATED_MESSAGE_INSERT + id); 967 } else if (match == MESSAGE_ID) { 968 db.execSQL(UPDATED_MESSAGE_DELETE + id); 969 } 970 result = db.update(TABLE_NAMES[table], values, whereWithId(id, selection), 971 selectionArgs); 972 break; 973 case BODY: 974 case MESSAGE: 975 case UPDATED_MESSAGE: 976 case ATTACHMENT: 977 case MAILBOX: 978 case ACCOUNT: 979 case HOSTAUTH: 980 result = db.update(TABLE_NAMES[table], values, selection, selectionArgs); 981 break; 982 default: 983 throw new IllegalArgumentException("Unknown URI " + uri); 984 } 985 986 getContext().getContentResolver().notifyChange(uri, null); 987 return result; 988 } 989 990 /* (non-Javadoc) 991 * @see android.content.ContentProvider#applyBatch(android.content.ContentProviderOperation) 992 */ 993 @Override 994 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 995 throws OperationApplicationException { 996 Context context = getContext(); 997 SQLiteDatabase db = getDatabase(context); 998 db.beginTransaction(); 999 mInTransaction = true; 1000 try { 1001 ContentProviderResult[] results = super.applyBatch(operations); 1002 db.setTransactionSuccessful(); 1003 return results; 1004 } finally { 1005 db.endTransaction(); 1006 mInTransaction = false; 1007 } 1008 } 1009} 1010