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