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