EmailProvider.java revision 91237e9dcb0a948f17488b464edabcea0f259d31
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; 33 34import android.accounts.AccountManager; 35import android.content.ContentProvider; 36import android.content.ContentProviderOperation; 37import android.content.ContentProviderResult; 38import android.content.ContentUris; 39import android.content.ContentValues; 40import android.content.Context; 41import android.content.OperationApplicationException; 42import android.content.UriMatcher; 43import android.database.Cursor; 44import android.database.SQLException; 45import android.database.sqlite.SQLiteDatabase; 46import android.database.sqlite.SQLiteException; 47import android.database.sqlite.SQLiteOpenHelper; 48import android.net.Uri; 49import android.util.Log; 50 51import java.io.File; 52import java.util.ArrayList; 53 54public class EmailProvider extends ContentProvider { 55 56 private static final String TAG = "EmailProvider"; 57 58 protected static final String DATABASE_NAME = "EmailProvider.db"; 59 protected static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; 60 61 public static final Uri INTEGRITY_CHECK_URI = 62 Uri.parse("content://" + EmailContent.AUTHORITY + "/integrityCheck"); 63 64 // Definitions for our queries looking for orphaned messages 65 private static final String[] ORPHANS_PROJECTION 66 = new String[] {MessageColumns.ID, MessageColumns.MAILBOX_KEY}; 67 private static final int ORPHANS_ID = 0; 68 private static final int ORPHANS_MAILBOX_KEY = 1; 69 70 private static final String WHERE_ID = EmailContent.RECORD_ID + "=?"; 71 72 private static final String[] COUNT_COLUMNS = new String[]{"count(*)"}; 73 74 // Any changes to the database format *must* include update-in-place code. 75 // Original version: 3 76 // Version 4: Database wipe required; changing AccountManager interface w/Exchange 77 // Version 5: Database wipe required; changing AccountManager interface w/Exchange 78 // Version 6: Adding Message.mServerTimeStamp column 79 // Version 7: Replace the mailbox_delete trigger with a version that removes orphaned messages 80 // from the Message_Deletes and Message_Updates tables 81 // Version 8: Add security flags column to accounts table 82 // Version 9: Add security sync key and signature to accounts table 83 public static final int DATABASE_VERSION = 9; 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 + AccountColumns.SECURITY_SYNC_KEY + " text, " 418 + AccountColumns.SIGNATURE + " text " 419 + ");"; 420 db.execSQL("create table " + Account.TABLE_NAME + s); 421 // Deleting an account deletes associated Mailboxes and HostAuth's 422 db.execSQL("create trigger account_delete before delete on " + Account.TABLE_NAME + 423 " begin delete from " + Mailbox.TABLE_NAME + 424 " where " + MailboxColumns.ACCOUNT_KEY + "=old." + EmailContent.RECORD_ID + 425 "; delete from " + HostAuth.TABLE_NAME + 426 " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_RECV + 427 "; delete from " + HostAuth.TABLE_NAME + 428 " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_SEND + 429 "; end"); 430 } 431 432 static void resetAccountTable(SQLiteDatabase db, int oldVersion, int newVersion) { 433 try { 434 db.execSQL("drop table " + Account.TABLE_NAME); 435 } catch (SQLException e) { 436 } 437 createAccountTable(db); 438 } 439 440 static void createHostAuthTable(SQLiteDatabase db) { 441 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 442 + HostAuthColumns.PROTOCOL + " text, " 443 + HostAuthColumns.ADDRESS + " text, " 444 + HostAuthColumns.PORT + " integer, " 445 + HostAuthColumns.FLAGS + " integer, " 446 + HostAuthColumns.LOGIN + " text, " 447 + HostAuthColumns.PASSWORD + " text, " 448 + HostAuthColumns.DOMAIN + " text, " 449 + HostAuthColumns.ACCOUNT_KEY + " integer" 450 + ");"; 451 db.execSQL("create table " + HostAuth.TABLE_NAME + s); 452 } 453 454 static void resetHostAuthTable(SQLiteDatabase db, int oldVersion, int newVersion) { 455 try { 456 db.execSQL("drop table " + HostAuth.TABLE_NAME); 457 } catch (SQLException e) { 458 } 459 createHostAuthTable(db); 460 } 461 462 static void createMailboxTable(SQLiteDatabase db) { 463 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 464 + MailboxColumns.DISPLAY_NAME + " text, " 465 + MailboxColumns.SERVER_ID + " text, " 466 + MailboxColumns.PARENT_SERVER_ID + " text, " 467 + MailboxColumns.ACCOUNT_KEY + " integer, " 468 + MailboxColumns.TYPE + " integer, " 469 + MailboxColumns.DELIMITER + " integer, " 470 + MailboxColumns.SYNC_KEY + " text, " 471 + MailboxColumns.SYNC_LOOKBACK + " integer, " 472 + MailboxColumns.SYNC_INTERVAL + " integer, " 473 + MailboxColumns.SYNC_TIME + " integer, " 474 + MailboxColumns.UNREAD_COUNT + " integer, " 475 + MailboxColumns.FLAG_VISIBLE + " integer, " 476 + MailboxColumns.FLAGS + " integer, " 477 + MailboxColumns.VISIBLE_LIMIT + " integer, " 478 + MailboxColumns.SYNC_STATUS + " text" 479 + ");"; 480 db.execSQL("create table " + Mailbox.TABLE_NAME + s); 481 db.execSQL("create index mailbox_" + MailboxColumns.SERVER_ID 482 + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.SERVER_ID + ")"); 483 db.execSQL("create index mailbox_" + MailboxColumns.ACCOUNT_KEY 484 + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.ACCOUNT_KEY + ")"); 485 // Deleting a Mailbox deletes associated Messages in all three tables 486 db.execSQL(TRIGGER_MAILBOX_DELETE); 487 } 488 489 static void resetMailboxTable(SQLiteDatabase db, int oldVersion, int newVersion) { 490 try { 491 db.execSQL("drop table " + Mailbox.TABLE_NAME); 492 } catch (SQLException e) { 493 } 494 createMailboxTable(db); 495 } 496 497 static void createAttachmentTable(SQLiteDatabase db) { 498 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 499 + AttachmentColumns.FILENAME + " text, " 500 + AttachmentColumns.MIME_TYPE + " text, " 501 + AttachmentColumns.SIZE + " integer, " 502 + AttachmentColumns.CONTENT_ID + " text, " 503 + AttachmentColumns.CONTENT_URI + " text, " 504 + AttachmentColumns.MESSAGE_KEY + " integer, " 505 + AttachmentColumns.LOCATION + " text, " 506 + AttachmentColumns.ENCODING + " text" 507 + ");"; 508 db.execSQL("create table " + Attachment.TABLE_NAME + s); 509 db.execSQL(createIndex(Attachment.TABLE_NAME, AttachmentColumns.MESSAGE_KEY)); 510 } 511 512 static void resetAttachmentTable(SQLiteDatabase db, int oldVersion, int newVersion) { 513 try { 514 db.execSQL("drop table " + Attachment.TABLE_NAME); 515 } catch (SQLException e) { 516 } 517 createAttachmentTable(db); 518 } 519 520 static void createBodyTable(SQLiteDatabase db) { 521 String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " 522 + BodyColumns.MESSAGE_KEY + " integer, " 523 + BodyColumns.HTML_CONTENT + " text, " 524 + BodyColumns.TEXT_CONTENT + " text, " 525 + BodyColumns.HTML_REPLY + " text, " 526 + BodyColumns.TEXT_REPLY + " text, " 527 + BodyColumns.SOURCE_MESSAGE_KEY + " text, " 528 + BodyColumns.INTRO_TEXT + " text" 529 + ");"; 530 db.execSQL("create table " + Body.TABLE_NAME + s); 531 db.execSQL(createIndex(Body.TABLE_NAME, BodyColumns.MESSAGE_KEY)); 532 } 533 534 static void upgradeBodyTable(SQLiteDatabase db, int oldVersion, int newVersion) { 535 if (oldVersion < 5) { 536 try { 537 db.execSQL("drop table " + Body.TABLE_NAME); 538 createBodyTable(db); 539 } catch (SQLException e) { 540 } 541 } else if (oldVersion == 5) { 542 try { 543 db.execSQL("alter table " + Body.TABLE_NAME 544 + " add " + BodyColumns.INTRO_TEXT + " text"); 545 } catch (SQLException e) { 546 // Shouldn't be needed unless we're debugging and interrupt the process 547 Log.w(TAG, "Exception upgrading EmailProviderBody.db from v5 to v6", e); 548 } 549 oldVersion = 6; 550 } 551 } 552 553 private SQLiteDatabase mDatabase; 554 private SQLiteDatabase mBodyDatabase; 555 556 public synchronized SQLiteDatabase getDatabase(Context context) { 557 // Always return the cached database, if we've got one 558 if (mDatabase != null) { 559 return mDatabase; 560 } 561 562 // Whenever we create or re-cache the databases, make sure that we haven't lost one 563 // to corruption 564 checkDatabases(); 565 566 DatabaseHelper helper = new DatabaseHelper(context, DATABASE_NAME); 567 mDatabase = helper.getWritableDatabase(); 568 if (mDatabase != null) { 569 mDatabase.setLockingEnabled(true); 570 BodyDatabaseHelper bodyHelper = new BodyDatabaseHelper(context, BODY_DATABASE_NAME); 571 mBodyDatabase = bodyHelper.getWritableDatabase(); 572 if (mBodyDatabase != null) { 573 mBodyDatabase.setLockingEnabled(true); 574 String bodyFileName = mBodyDatabase.getPath(); 575 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); 576 } 577 } 578 579 // Check for any orphaned Messages in the updated/deleted tables 580 deleteOrphans(mDatabase, Message.UPDATED_TABLE_NAME); 581 deleteOrphans(mDatabase, Message.DELETED_TABLE_NAME); 582 583 return mDatabase; 584 } 585 586 /*package*/ static SQLiteDatabase getReadableDatabase(Context context) { 587 DatabaseHelper helper = new EmailProvider().new DatabaseHelper(context, DATABASE_NAME); 588 return helper.getReadableDatabase(); 589 } 590 591 /*package*/ static void deleteOrphans(SQLiteDatabase database, String tableName) { 592 if (database != null) { 593 // We'll look at all of the items in the table; there won't be many typically 594 Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); 595 // Usually, there will be nothing in these tables, so make a quick check 596 try { 597 if (c.getCount() == 0) return; 598 ArrayList<Long> foundMailboxes = new ArrayList<Long>(); 599 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>(); 600 ArrayList<Long> deleteList = new ArrayList<Long>(); 601 String[] bindArray = new String[1]; 602 while (c.moveToNext()) { 603 // Get the mailbox key and see if we've already found this mailbox 604 // If so, we're fine 605 long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY); 606 // If we already know this mailbox doesn't exist, mark the message for deletion 607 if (notFoundMailboxes.contains(mailboxId)) { 608 deleteList.add(c.getLong(ORPHANS_ID)); 609 // If we don't know about this mailbox, we'll try to find it 610 } else if (!foundMailboxes.contains(mailboxId)) { 611 bindArray[0] = Long.toString(mailboxId); 612 Cursor boxCursor = database.query(Mailbox.TABLE_NAME, 613 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null); 614 try { 615 // If it exists, we'll add it to the "found" mailboxes 616 if (boxCursor.moveToFirst()) { 617 foundMailboxes.add(mailboxId); 618 // Otherwise, we'll add to "not found" and mark the message for deletion 619 } else { 620 notFoundMailboxes.add(mailboxId); 621 deleteList.add(c.getLong(ORPHANS_ID)); 622 } 623 } finally { 624 boxCursor.close(); 625 } 626 } 627 } 628 // Now, delete the orphan messages 629 for (long messageId: deleteList) { 630 bindArray[0] = Long.toString(messageId); 631 database.delete(tableName, WHERE_ID, bindArray); 632 } 633 } finally { 634 c.close(); 635 } 636 } 637 } 638 639 private class BodyDatabaseHelper extends SQLiteOpenHelper { 640 BodyDatabaseHelper(Context context, String name) { 641 super(context, name, null, BODY_DATABASE_VERSION); 642 } 643 644 @Override 645 public void onCreate(SQLiteDatabase db) { 646 Log.d(TAG, "Creating EmailProviderBody database"); 647 createBodyTable(db); 648 } 649 650 @Override 651 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 652 upgradeBodyTable(db, oldVersion, newVersion); 653 } 654 655 @Override 656 public void onOpen(SQLiteDatabase db) { 657 } 658 } 659 660 private class DatabaseHelper extends SQLiteOpenHelper { 661 Context mContext; 662 663 DatabaseHelper(Context context, String name) { 664 super(context, name, null, DATABASE_VERSION); 665 mContext = context; 666 } 667 668 @Override 669 public void onCreate(SQLiteDatabase db) { 670 Log.d(TAG, "Creating EmailProvider database"); 671 // Create all tables here; each class has its own method 672 createMessageTable(db); 673 createAttachmentTable(db); 674 createMailboxTable(db); 675 createHostAuthTable(db); 676 createAccountTable(db); 677 } 678 679 @Override 680 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 681 // For versions prior to 5, delete all data 682 // Versions >= 5 require that data be preserved! 683 if (oldVersion < 5) { 684 android.accounts.Account[] accounts = AccountManager.get(mContext) 685 .getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE); 686 for (android.accounts.Account account: accounts) { 687 AccountManager.get(mContext).removeAccount(account, null, null); 688 } 689 resetMessageTable(db, oldVersion, newVersion); 690 resetAttachmentTable(db, oldVersion, newVersion); 691 resetMailboxTable(db, oldVersion, newVersion); 692 resetHostAuthTable(db, oldVersion, newVersion); 693 resetAccountTable(db, oldVersion, newVersion); 694 return; 695 } 696 if (oldVersion == 5) { 697 // Message Tables: Add SyncColumns.SERVER_TIMESTAMP 698 try { 699 db.execSQL("alter table " + Message.TABLE_NAME 700 + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";"); 701 db.execSQL("alter table " + Message.UPDATED_TABLE_NAME 702 + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";"); 703 db.execSQL("alter table " + Message.DELETED_TABLE_NAME 704 + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";"); 705 } catch (SQLException e) { 706 // Shouldn't be needed unless we're debugging and interrupt the process 707 Log.w(TAG, "Exception upgrading EmailProvider.db from v5 to v6", e); 708 } 709 oldVersion = 6; 710 } 711 if (oldVersion == 6) { 712 // Use the newer mailbox_delete trigger 713 db.execSQL("drop trigger mailbox_delete;"); 714 db.execSQL(TRIGGER_MAILBOX_DELETE); 715 oldVersion = 7; 716 } 717 if (oldVersion == 7) { 718 // add the security (provisioning) column 719 try { 720 db.execSQL("alter table " + Account.TABLE_NAME 721 + " add column " + AccountColumns.SECURITY_FLAGS + " integer" + ";"); 722 } catch (SQLException e) { 723 // Shouldn't be needed unless we're debugging and interrupt the process 724 Log.w(TAG, "Exception upgrading EmailProvider.db from 7 to 8 " + e); 725 } 726 oldVersion = 8; 727 } 728 if (oldVersion == 8) { 729 // accounts: add security sync key & user signature columns 730 try { 731 db.execSQL("alter table " + Account.TABLE_NAME 732 + " add column " + AccountColumns.SECURITY_SYNC_KEY + " text" + ";"); 733 db.execSQL("alter table " + Account.TABLE_NAME 734 + " add column " + AccountColumns.SIGNATURE + " text" + ";"); 735 } catch (SQLException e) { 736 // Shouldn't be needed unless we're debugging and interrupt the process 737 Log.w(TAG, "Exception upgrading EmailProvider.db from 8 to 9 " + e); 738 } 739 oldVersion = 9; 740 } 741 } 742 743 @Override 744 public void onOpen(SQLiteDatabase db) { 745 } 746 } 747 748 @Override 749 public int delete(Uri uri, String selection, String[] selectionArgs) { 750 final int match = sURIMatcher.match(uri); 751 Context context = getContext(); 752 // Pick the correct database for this operation 753 // If we're in a transaction already (which would happen during applyBatch), then the 754 // body database is already attached to the email database and any attempt to use the 755 // body database directly will result in a SQLiteException (the database is locked) 756 SQLiteDatabase db = getDatabase(context); 757 int table = match >> BASE_SHIFT; 758 String id = "0"; 759 boolean messageDeletion = false; 760 761 if (Email.LOGD) { 762 Log.v(TAG, "EmailProvider.delete: uri=" + uri + ", match is " + match); 763 } 764 765 int result = -1; 766 767 try { 768 switch (match) { 769 // These are cases in which one or more Messages might get deleted, either by 770 // cascade or explicitly 771 case MAILBOX_ID: 772 case MAILBOX: 773 case ACCOUNT_ID: 774 case ACCOUNT: 775 case MESSAGE: 776 case SYNCED_MESSAGE_ID: 777 case MESSAGE_ID: 778 // Handle lost Body records here, since this cannot be done in a trigger 779 // The process is: 780 // 1) Begin a transaction, ensuring that both databases are affected atomically 781 // 2) Do the requested deletion, with cascading deletions handled in triggers 782 // 3) End the transaction, committing all changes atomically 783 // 784 // Bodies are auto-deleted here; Attachments are auto-deleted via trigger 785 786 messageDeletion = true; 787 db.beginTransaction(); 788 break; 789 } 790 switch (match) { 791 case BODY_ID: 792 case DELETED_MESSAGE_ID: 793 case SYNCED_MESSAGE_ID: 794 case MESSAGE_ID: 795 case UPDATED_MESSAGE_ID: 796 case ATTACHMENT_ID: 797 case MAILBOX_ID: 798 case ACCOUNT_ID: 799 case HOSTAUTH_ID: 800 id = uri.getPathSegments().get(1); 801 if (match == SYNCED_MESSAGE_ID) { 802 // For synced messages, first copy the old message to the deleted table and 803 // delete it from the updated table (in case it was updated first) 804 // Note that this is all within a transaction, for atomicity 805 db.execSQL(DELETED_MESSAGE_INSERT + id); 806 db.execSQL(UPDATED_MESSAGE_DELETE + id); 807 } 808 result = db.delete(TABLE_NAMES[table], whereWithId(id, selection), 809 selectionArgs); 810 break; 811 case ATTACHMENTS_MESSAGE_ID: 812 // All attachments for the given message 813 id = uri.getPathSegments().get(2); 814 result = db.delete(TABLE_NAMES[table], 815 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs); 816 break; 817 818 case BODY: 819 case MESSAGE: 820 case DELETED_MESSAGE: 821 case UPDATED_MESSAGE: 822 case ATTACHMENT: 823 case MAILBOX: 824 case ACCOUNT: 825 case HOSTAUTH: 826 result = db.delete(TABLE_NAMES[table], selection, selectionArgs); 827 break; 828 829 default: 830 throw new IllegalArgumentException("Unknown URI " + uri); 831 } 832 if (messageDeletion) { 833 if (match == MESSAGE_ID) { 834 // Delete the Body record associated with the deleted message 835 db.execSQL(DELETE_BODY + id); 836 } else { 837 // Delete any orphaned Body records 838 db.execSQL(DELETE_ORPHAN_BODIES); 839 } 840 db.setTransactionSuccessful(); 841 } 842 } catch (SQLiteException e) { 843 checkDatabases(); 844 throw e; 845 } finally { 846 if (messageDeletion) { 847 db.endTransaction(); 848 } 849 } 850 getContext().getContentResolver().notifyChange(uri, null); 851 return result; 852 } 853 854 @Override 855 // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) 856 public String getType(Uri uri) { 857 int match = sURIMatcher.match(uri); 858 switch (match) { 859 case BODY_ID: 860 return "vnd.android.cursor.item/email-body"; 861 case BODY: 862 return "vnd.android.cursor.dir/email-message"; 863 case UPDATED_MESSAGE_ID: 864 case MESSAGE_ID: 865 return "vnd.android.cursor.item/email-message"; 866 case MAILBOX_MESSAGES: 867 case UPDATED_MESSAGE: 868 case MESSAGE: 869 return "vnd.android.cursor.dir/email-message"; 870 case ACCOUNT_MAILBOXES: 871 case MAILBOX: 872 return "vnd.android.cursor.dir/email-mailbox"; 873 case MAILBOX_ID: 874 return "vnd.android.cursor.item/email-mailbox"; 875 case ACCOUNT: 876 return "vnd.android.cursor.dir/email-account"; 877 case ACCOUNT_ID: 878 return "vnd.android.cursor.item/email-account"; 879 case ATTACHMENTS_MESSAGE_ID: 880 case ATTACHMENT: 881 return "vnd.android.cursor.dir/email-attachment"; 882 case ATTACHMENT_ID: 883 return "vnd.android.cursor.item/email-attachment"; 884 case HOSTAUTH: 885 return "vnd.android.cursor.dir/email-hostauth"; 886 case HOSTAUTH_ID: 887 return "vnd.android.cursor.item/email-hostauth"; 888 default: 889 throw new IllegalArgumentException("Unknown URI " + uri); 890 } 891 } 892 893 @Override 894 public Uri insert(Uri uri, ContentValues values) { 895 int match = sURIMatcher.match(uri); 896 Context context = getContext(); 897 // See the comment at delete(), above 898 SQLiteDatabase db = getDatabase(context); 899 int table = match >> BASE_SHIFT; 900 long id; 901 902 if (Email.LOGD) { 903 Log.v(TAG, "EmailProvider.insert: uri=" + uri + ", match is " + match); 904 } 905 906 Uri resultUri = null; 907 908 try { 909 switch (match) { 910 case UPDATED_MESSAGE: 911 case DELETED_MESSAGE: 912 case BODY: 913 case MESSAGE: 914 case ATTACHMENT: 915 case MAILBOX: 916 case ACCOUNT: 917 case HOSTAUTH: 918 id = db.insert(TABLE_NAMES[table], "foo", values); 919 resultUri = ContentUris.withAppendedId(uri, id); 920 // Clients shouldn't normally be adding rows to these tables, as they are 921 // maintained by triggers. However, we need to be able to do this for unit 922 // testing, so we allow the insert and then throw the same exception that we 923 // would if this weren't allowed. 924 if (match == UPDATED_MESSAGE || match == DELETED_MESSAGE) { 925 throw new IllegalArgumentException("Unknown URL " + uri); 926 } 927 break; 928 case MAILBOX_ID: 929 // This implies adding a message to a mailbox 930 // Hmm, a problem here is that we can't link the account as well, so it must be 931 // already in the values... 932 id = Long.parseLong(uri.getPathSegments().get(1)); 933 values.put(MessageColumns.MAILBOX_KEY, id); 934 resultUri = insert(Message.CONTENT_URI, values); 935 break; 936 case MESSAGE_ID: 937 // This implies adding an attachment to a message. 938 id = Long.parseLong(uri.getPathSegments().get(1)); 939 values.put(AttachmentColumns.MESSAGE_KEY, id); 940 resultUri = insert(Attachment.CONTENT_URI, values); 941 break; 942 case ACCOUNT_ID: 943 // This implies adding a mailbox to an account. 944 id = Long.parseLong(uri.getPathSegments().get(1)); 945 values.put(MailboxColumns.ACCOUNT_KEY, id); 946 resultUri = insert(Mailbox.CONTENT_URI, values); 947 break; 948 case ATTACHMENTS_MESSAGE_ID: 949 id = db.insert(TABLE_NAMES[table], "foo", values); 950 resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, id); 951 break; 952 default: 953 throw new IllegalArgumentException("Unknown URL " + uri); 954 } 955 } catch (SQLiteException e) { 956 checkDatabases(); 957 throw e; 958 } 959 960 // Notify with the base uri, not the new uri (nobody is watching a new record) 961 getContext().getContentResolver().notifyChange(uri, null); 962 return resultUri; 963 } 964 965 @Override 966 public boolean onCreate() { 967 checkDatabases(); 968 return false; 969 } 970 971 /** 972 * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must 973 * always be in sync (i.e. there are two database or NO databases). This code will delete 974 * any "orphan" database, so that both will be created together. Note that an "orphan" database 975 * will exist after either of the individual databases is deleted due to data corruption. 976 */ 977 public void checkDatabases() { 978 // Uncache the databases 979 if (mDatabase != null) { 980 mDatabase = null; 981 } 982 if (mBodyDatabase != null) { 983 mBodyDatabase = null; 984 } 985 // Look for orphans, and delete as necessary; these must always be in sync 986 File databaseFile = getContext().getDatabasePath(DATABASE_NAME); 987 File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME); 988 989 // TODO Make sure attachments are deleted 990 if (databaseFile.exists() && !bodyFile.exists()) { 991 Log.w(TAG, "Deleting orphaned EmailProvider database..."); 992 databaseFile.delete(); 993 } else if (bodyFile.exists() && !databaseFile.exists()) { 994 Log.w(TAG, "Deleting orphaned EmailProviderBody database..."); 995 bodyFile.delete(); 996 } 997 } 998 999 @Override 1000 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1001 String sortOrder) { 1002 Cursor c = null; 1003 Uri notificationUri = EmailContent.CONTENT_URI; 1004 int match = sURIMatcher.match(uri); 1005 Context context = getContext(); 1006 // See the comment at delete(), above 1007 SQLiteDatabase db = getDatabase(context); 1008 int table = match >> BASE_SHIFT; 1009 String id; 1010 1011 if (Email.LOGD) { 1012 Log.v(TAG, "EmailProvider.query: uri=" + uri + ", match is " + match); 1013 } 1014 1015 try { 1016 switch (match) { 1017 case BODY: 1018 case MESSAGE: 1019 case UPDATED_MESSAGE: 1020 case DELETED_MESSAGE: 1021 case ATTACHMENT: 1022 case MAILBOX: 1023 case ACCOUNT: 1024 case HOSTAUTH: 1025 c = db.query(TABLE_NAMES[table], projection, 1026 selection, selectionArgs, null, null, sortOrder); 1027 break; 1028 case BODY_ID: 1029 case MESSAGE_ID: 1030 case DELETED_MESSAGE_ID: 1031 case UPDATED_MESSAGE_ID: 1032 case ATTACHMENT_ID: 1033 case MAILBOX_ID: 1034 case ACCOUNT_ID: 1035 case HOSTAUTH_ID: 1036 id = uri.getPathSegments().get(1); 1037 c = db.query(TABLE_NAMES[table], projection, 1038 whereWithId(id, selection), selectionArgs, null, null, sortOrder); 1039 break; 1040 case ATTACHMENTS_MESSAGE_ID: 1041 // All attachments for the given message 1042 id = uri.getPathSegments().get(2); 1043 c = db.query(Attachment.TABLE_NAME, projection, 1044 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), 1045 selectionArgs, null, null, sortOrder); 1046 break; 1047 default: 1048 throw new IllegalArgumentException("Unknown URI " + uri); 1049 } 1050 } catch (SQLiteException e) { 1051 checkDatabases(); 1052 throw e; 1053 } 1054 1055 if ((c != null) && !isTemporary()) { 1056 c.setNotificationUri(getContext().getContentResolver(), notificationUri); 1057 } 1058 return c; 1059 } 1060 1061 private String whereWithId(String id, String selection) { 1062 StringBuilder sb = new StringBuilder(256); 1063 sb.append("_id="); 1064 sb.append(id); 1065 if (selection != null) { 1066 sb.append(" AND ("); 1067 sb.append(selection); 1068 sb.append(')'); 1069 } 1070 return sb.toString(); 1071 } 1072 1073 /** 1074 * Combine a locally-generated selection with a user-provided selection 1075 * 1076 * This introduces risk that the local selection might insert incorrect chars 1077 * into the SQL, so use caution. 1078 * 1079 * @param where locally-generated selection, must not be null 1080 * @param selection user-provided selection, may be null 1081 * @return a single selection string 1082 */ 1083 private String whereWith(String where, String selection) { 1084 if (selection == null) { 1085 return where; 1086 } 1087 StringBuilder sb = new StringBuilder(where); 1088 sb.append(" AND ("); 1089 sb.append(selection); 1090 sb.append(')'); 1091 1092 return sb.toString(); 1093 } 1094 1095 @Override 1096 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1097 int match = sURIMatcher.match(uri); 1098 Context context = getContext(); 1099 // See the comment at delete(), above 1100 SQLiteDatabase db = getDatabase(context); 1101 int table = match >> BASE_SHIFT; 1102 int result; 1103 1104 if (Email.LOGD) { 1105 Log.v(TAG, "EmailProvider.update: uri=" + uri + ", match is " + match); 1106 } 1107 1108 // We do NOT allow setting of unreadCount via the provider 1109 // This column is maintained via triggers 1110 if (match == MAILBOX_ID || match == MAILBOX) { 1111 values.remove(MailboxColumns.UNREAD_COUNT); 1112 } 1113 1114 // Handle this special case the fastest possible way 1115 if (uri == INTEGRITY_CHECK_URI) { 1116 checkDatabases(); 1117 return 0; 1118 } 1119 1120 String id; 1121 try { 1122 switch (match) { 1123 case MAILBOX_ID_ADD_TO_FIELD: 1124 case ACCOUNT_ID_ADD_TO_FIELD: 1125 db.beginTransaction(); 1126 id = uri.getPathSegments().get(1); 1127 String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME); 1128 Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME); 1129 if (field == null || add == null) { 1130 throw new IllegalArgumentException("No field/add specified " + uri); 1131 } 1132 Cursor c = db.query(TABLE_NAMES[table], 1133 new String[] {EmailContent.RECORD_ID, field}, 1134 whereWithId(id, selection), 1135 selectionArgs, null, null, null); 1136 try { 1137 result = 0; 1138 ContentValues cv = new ContentValues(); 1139 String[] bind = new String[1]; 1140 while (c.moveToNext()) { 1141 bind[0] = c.getString(0); 1142 long value = c.getLong(1) + add; 1143 cv.put(field, value); 1144 result = db.update(TABLE_NAMES[table], cv, ID_EQUALS, bind); 1145 } 1146 } finally { 1147 c.close(); 1148 } 1149 db.setTransactionSuccessful(); 1150 db.endTransaction(); 1151 break; 1152 case BODY_ID: 1153 case MESSAGE_ID: 1154 case SYNCED_MESSAGE_ID: 1155 case UPDATED_MESSAGE_ID: 1156 case ATTACHMENT_ID: 1157 case MAILBOX_ID: 1158 case ACCOUNT_ID: 1159 case HOSTAUTH_ID: 1160 id = uri.getPathSegments().get(1); 1161 if (match == SYNCED_MESSAGE_ID) { 1162 // For synced messages, first copy the old message to the updated table 1163 // Note the insert or ignore semantics, guaranteeing that only the first 1164 // update will be reflected in the updated message table; therefore this row 1165 // will always have the "original" data 1166 db.execSQL(UPDATED_MESSAGE_INSERT + id); 1167 } else if (match == MESSAGE_ID) { 1168 db.execSQL(UPDATED_MESSAGE_DELETE + id); 1169 } 1170 result = db.update(TABLE_NAMES[table], values, whereWithId(id, selection), 1171 selectionArgs); 1172 break; 1173 case BODY: 1174 case MESSAGE: 1175 case UPDATED_MESSAGE: 1176 case ATTACHMENT: 1177 case MAILBOX: 1178 case ACCOUNT: 1179 case HOSTAUTH: 1180 result = db.update(TABLE_NAMES[table], values, selection, selectionArgs); 1181 break; 1182 default: 1183 throw new IllegalArgumentException("Unknown URI " + uri); 1184 } 1185 } catch (SQLiteException e) { 1186 checkDatabases(); 1187 throw e; 1188 } 1189 1190 getContext().getContentResolver().notifyChange(uri, null); 1191 return result; 1192 } 1193 1194 /* (non-Javadoc) 1195 * @see android.content.ContentProvider#applyBatch(android.content.ContentProviderOperation) 1196 */ 1197 @Override 1198 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1199 throws OperationApplicationException { 1200 Context context = getContext(); 1201 SQLiteDatabase db = getDatabase(context); 1202 db.beginTransaction(); 1203 try { 1204 ContentProviderResult[] results = super.applyBatch(operations); 1205 db.setTransactionSuccessful(); 1206 return results; 1207 } finally { 1208 db.endTransaction(); 1209 } 1210 } 1211} 1212