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