EmailProvider.java revision 63d60d9eb182d583d08e6d4b6dc11b392dede6f1
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 android.content.ContentProvider; 20import android.content.ContentProviderOperation; 21import android.content.ContentProviderResult; 22import android.content.ContentResolver; 23import android.content.ContentUris; 24import android.content.ContentValues; 25import android.content.Context; 26import android.content.Intent; 27import android.content.OperationApplicationException; 28import android.content.UriMatcher; 29import android.database.ContentObserver; 30import android.database.Cursor; 31import android.database.MatrixCursor; 32import android.database.sqlite.SQLiteDatabase; 33import android.database.sqlite.SQLiteException; 34import android.net.Uri; 35import android.provider.BaseColumns; 36import android.text.TextUtils; 37import android.util.Log; 38 39import com.android.common.content.ProjectionMap; 40import com.android.email.Email; 41import com.android.email.Preferences; 42import com.android.email.R; 43import com.android.email.provider.ContentCache.CacheToken; 44import com.android.email.service.AttachmentDownloadService; 45import com.android.emailcommon.Logging; 46import com.android.emailcommon.provider.Account; 47import com.android.emailcommon.provider.EmailContent; 48import com.android.emailcommon.provider.EmailContent.AccountColumns; 49import com.android.emailcommon.provider.EmailContent.Attachment; 50import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 51import com.android.emailcommon.provider.EmailContent.Body; 52import com.android.emailcommon.provider.EmailContent.BodyColumns; 53import com.android.emailcommon.provider.EmailContent.MailboxColumns; 54import com.android.emailcommon.provider.EmailContent.Message; 55import com.android.emailcommon.provider.EmailContent.MessageColumns; 56import com.android.emailcommon.provider.EmailContent.PolicyColumns; 57import com.android.emailcommon.provider.EmailContent.SyncColumns; 58import com.android.emailcommon.provider.HostAuth; 59import com.android.emailcommon.provider.Mailbox; 60import com.android.emailcommon.provider.Policy; 61import com.android.emailcommon.provider.QuickResponse; 62import com.android.mail.providers.UIProvider; 63import com.android.mail.providers.UIProvider.ConversationPriority; 64import com.android.mail.providers.UIProvider.ConversationSendingState; 65import com.google.common.annotations.VisibleForTesting; 66 67import java.io.File; 68import java.util.ArrayList; 69import java.util.Arrays; 70import java.util.Collection; 71import java.util.HashMap; 72import java.util.List; 73import java.util.Map; 74 75public class EmailProvider extends ContentProvider { 76 77 private static final String TAG = "EmailProvider"; 78 79 protected static final String DATABASE_NAME = "EmailProvider.db"; 80 protected static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; 81 protected static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db"; 82 83 public static final String ACTION_ATTACHMENT_UPDATED = "com.android.email.ATTACHMENT_UPDATED"; 84 public static final String ATTACHMENT_UPDATED_EXTRA_FLAGS = 85 "com.android.email.ATTACHMENT_UPDATED_FLAGS"; 86 87 /** 88 * Notifies that changes happened. Certain UI components, e.g., widgets, can register for this 89 * {@link android.content.Intent} and update accordingly. However, this can be very broad and 90 * is NOT the preferred way of getting notification. 91 */ 92 public static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED = 93 "com.android.email.MESSAGE_LIST_DATASET_CHANGED"; 94 95 public static final String EMAIL_MESSAGE_MIME_TYPE = 96 "vnd.android.cursor.item/email-message"; 97 public static final String EMAIL_ATTACHMENT_MIME_TYPE = 98 "vnd.android.cursor.item/email-attachment"; 99 100 public static final Uri INTEGRITY_CHECK_URI = 101 Uri.parse("content://" + EmailContent.AUTHORITY + "/integrityCheck"); 102 public static final Uri ACCOUNT_BACKUP_URI = 103 Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup"); 104 105 /** Appended to the notification URI for delete operations */ 106 public static final String NOTIFICATION_OP_DELETE = "delete"; 107 /** Appended to the notification URI for insert operations */ 108 public static final String NOTIFICATION_OP_INSERT = "insert"; 109 /** Appended to the notification URI for update operations */ 110 public static final String NOTIFICATION_OP_UPDATE = "update"; 111 112 // Definitions for our queries looking for orphaned messages 113 private static final String[] ORPHANS_PROJECTION 114 = new String[] {MessageColumns.ID, MessageColumns.MAILBOX_KEY}; 115 private static final int ORPHANS_ID = 0; 116 private static final int ORPHANS_MAILBOX_KEY = 1; 117 118 private static final String WHERE_ID = EmailContent.RECORD_ID + "=?"; 119 120 // This is not a hard limit on accounts, per se, but beyond this, we can't guarantee that all 121 // critical mailboxes, host auth's, accounts, and policies are cached 122 private static final int MAX_CACHED_ACCOUNTS = 16; 123 // Inbox, Drafts, Sent, Outbox, Trash, and Search (these boxes are cached when possible) 124 private static final int NUM_ALWAYS_CACHED_MAILBOXES = 6; 125 126 // We'll cache the following four tables; sizes are best estimates of effective values 127 private final ContentCache mCacheAccount = 128 new ContentCache("Account", Account.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS); 129 private final ContentCache mCacheHostAuth = 130 new ContentCache("HostAuth", HostAuth.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS * 2); 131 /*package*/ final ContentCache mCacheMailbox = 132 new ContentCache("Mailbox", Mailbox.CONTENT_PROJECTION, 133 MAX_CACHED_ACCOUNTS * (NUM_ALWAYS_CACHED_MAILBOXES + 2)); 134 private final ContentCache mCacheMessage = 135 new ContentCache("Message", Message.CONTENT_PROJECTION, 8); 136 private final ContentCache mCachePolicy = 137 new ContentCache("Policy", Policy.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS); 138 139 private static final int ACCOUNT_BASE = 0; 140 private static final int ACCOUNT = ACCOUNT_BASE; 141 private static final int ACCOUNT_ID = ACCOUNT_BASE + 1; 142 private static final int ACCOUNT_ID_ADD_TO_FIELD = ACCOUNT_BASE + 2; 143 private static final int ACCOUNT_RESET_NEW_COUNT = ACCOUNT_BASE + 3; 144 private static final int ACCOUNT_RESET_NEW_COUNT_ID = ACCOUNT_BASE + 4; 145 private static final int ACCOUNT_DEFAULT_ID = ACCOUNT_BASE + 5; 146 147 private static final int MAILBOX_BASE = 0x1000; 148 private static final int MAILBOX = MAILBOX_BASE; 149 private static final int MAILBOX_ID = MAILBOX_BASE + 1; 150 private static final int MAILBOX_ID_FROM_ACCOUNT_AND_TYPE = MAILBOX_BASE + 2; 151 private static final int MAILBOX_ID_ADD_TO_FIELD = MAILBOX_BASE + 2; 152 153 private static final int MESSAGE_BASE = 0x2000; 154 private static final int MESSAGE = MESSAGE_BASE; 155 private static final int MESSAGE_ID = MESSAGE_BASE + 1; 156 private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2; 157 158 private static final int ATTACHMENT_BASE = 0x3000; 159 private static final int ATTACHMENT = ATTACHMENT_BASE; 160 private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1; 161 private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2; 162 163 private static final int HOSTAUTH_BASE = 0x4000; 164 private static final int HOSTAUTH = HOSTAUTH_BASE; 165 private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1; 166 167 private static final int UPDATED_MESSAGE_BASE = 0x5000; 168 private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE; 169 private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1; 170 171 private static final int DELETED_MESSAGE_BASE = 0x6000; 172 private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE; 173 private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1; 174 175 private static final int POLICY_BASE = 0x7000; 176 private static final int POLICY = POLICY_BASE; 177 private static final int POLICY_ID = POLICY_BASE + 1; 178 179 private static final int QUICK_RESPONSE_BASE = 0x8000; 180 private static final int QUICK_RESPONSE = QUICK_RESPONSE_BASE; 181 private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1; 182 private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2; 183 184 private static final int UI_BASE = 0x9000; 185 private static final int UI_FOLDERS = UI_BASE; 186 private static final int UI_MESSAGES = UI_BASE + 1; 187 private static final int UI_MESSAGE = UI_BASE + 2; 188 private static final int UI_SENDMAIL = UI_BASE + 3; 189 private static final int UI_UNDO = UI_BASE + 4; 190 private static final int UI_SAVEDRAFT = UI_BASE + 5; 191 private static final int UI_UPDATEDRAFT = UI_BASE + 6; 192 private static final int UI_SENDDRAFT = UI_BASE + 7; 193 194 // MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS 195 private static final int LAST_EMAIL_PROVIDER_DB_BASE = UI_BASE; 196 197 // DO NOT CHANGE BODY_BASE!! 198 private static final int BODY_BASE = LAST_EMAIL_PROVIDER_DB_BASE + 0x1000; 199 private static final int BODY = BODY_BASE; 200 private static final int BODY_ID = BODY_BASE + 1; 201 202 private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc. 203 204 // TABLE_NAMES MUST remain in the order of the BASE constants above (e.g. ACCOUNT_BASE = 0x0000, 205 // MESSAGE_BASE = 0x1000, etc.) 206 private static final String[] TABLE_NAMES = { 207 Account.TABLE_NAME, 208 Mailbox.TABLE_NAME, 209 Message.TABLE_NAME, 210 Attachment.TABLE_NAME, 211 HostAuth.TABLE_NAME, 212 Message.UPDATED_TABLE_NAME, 213 Message.DELETED_TABLE_NAME, 214 Policy.TABLE_NAME, 215 QuickResponse.TABLE_NAME, 216 null, // UI 217 Body.TABLE_NAME, 218 }; 219 220 // CONTENT_CACHES MUST remain in the order of the BASE constants above 221 private final ContentCache[] mContentCaches = { 222 mCacheAccount, 223 mCacheMailbox, 224 mCacheMessage, 225 null, // Attachment 226 mCacheHostAuth, 227 null, // Updated message 228 null, // Deleted message 229 mCachePolicy, 230 null, // Quick response 231 null, // Body 232 null // UI 233 }; 234 235 // CACHE_PROJECTIONS MUST remain in the order of the BASE constants above 236 private static final String[][] CACHE_PROJECTIONS = { 237 Account.CONTENT_PROJECTION, 238 Mailbox.CONTENT_PROJECTION, 239 Message.CONTENT_PROJECTION, 240 null, // Attachment 241 HostAuth.CONTENT_PROJECTION, 242 null, // Updated message 243 null, // Deleted message 244 Policy.CONTENT_PROJECTION, 245 null, // Quick response 246 null, // Body 247 null // UI 248 }; 249 250 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 251 252 private static final String MAILBOX_PRE_CACHE_SELECTION = MailboxColumns.TYPE + " IN (" + 253 Mailbox.TYPE_INBOX + "," + Mailbox.TYPE_DRAFTS + "," + Mailbox.TYPE_TRASH + "," + 254 Mailbox.TYPE_SENT + "," + Mailbox.TYPE_SEARCH + "," + Mailbox.TYPE_OUTBOX + ")"; 255 256 /** 257 * Let's only generate these SQL strings once, as they are used frequently 258 * Note that this isn't relevant for table creation strings, since they are used only once 259 */ 260 private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " + 261 Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 262 EmailContent.RECORD_ID + '='; 263 264 private static final String UPDATED_MESSAGE_DELETE = "delete from " + 265 Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '='; 266 267 private static final String DELETED_MESSAGE_INSERT = "insert or replace into " + 268 Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 269 EmailContent.RECORD_ID + '='; 270 271 private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME + 272 " where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY + 273 " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " + 274 Message.TABLE_NAME + ')'; 275 276 private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME + 277 " where " + BodyColumns.MESSAGE_KEY + '='; 278 279 private static final String ID_EQUALS = EmailContent.RECORD_ID + "=?"; 280 281 private static final ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 282 283 public static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; 284 285 // For undo handling 286 private int mLastSequence = -1; 287 private ArrayList<ContentProviderOperation> mLastSequenceOps = 288 new ArrayList<ContentProviderOperation>(); 289 290 // Query parameter indicating the command came from UIProvider 291 private static final String IS_UIPROVIDER = "is_uiprovider"; 292 293 static { 294 // Email URI matching table 295 UriMatcher matcher = sURIMatcher; 296 297 // All accounts 298 matcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT); 299 // A specific account 300 // insert into this URI causes a mailbox to be added to the account 301 matcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID); 302 matcher.addURI(EmailContent.AUTHORITY, "account/default", ACCOUNT_DEFAULT_ID); 303 304 // Special URI to reset the new message count. Only update works, and content values 305 // will be ignored. 306 matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount", 307 ACCOUNT_RESET_NEW_COUNT); 308 matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount/#", 309 ACCOUNT_RESET_NEW_COUNT_ID); 310 311 // All mailboxes 312 matcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX); 313 // A specific mailbox 314 // insert into this URI causes a message to be added to the mailbox 315 // ** NOTE For now, the accountKey must be set manually in the values! 316 matcher.addURI(EmailContent.AUTHORITY, "mailbox/#", MAILBOX_ID); 317 matcher.addURI(EmailContent.AUTHORITY, "mailboxIdFromAccountAndType/#/#", 318 MAILBOX_ID_FROM_ACCOUNT_AND_TYPE); 319 // All messages 320 matcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE); 321 // A specific message 322 // insert into this URI causes an attachment to be added to the message 323 matcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID); 324 325 // A specific attachment 326 matcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT); 327 // A specific attachment (the header information) 328 matcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID); 329 // The attachments of a specific message (query only) (insert & delete TBD) 330 matcher.addURI(EmailContent.AUTHORITY, "attachment/message/#", 331 ATTACHMENTS_MESSAGE_ID); 332 333 // All mail bodies 334 matcher.addURI(EmailContent.AUTHORITY, "body", BODY); 335 // A specific mail body 336 matcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID); 337 338 // All hostauth records 339 matcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH); 340 // A specific hostauth 341 matcher.addURI(EmailContent.AUTHORITY, "hostauth/#", HOSTAUTH_ID); 342 343 // Atomically a constant value to a particular field of a mailbox/account 344 matcher.addURI(EmailContent.AUTHORITY, "mailboxIdAddToField/#", 345 MAILBOX_ID_ADD_TO_FIELD); 346 matcher.addURI(EmailContent.AUTHORITY, "accountIdAddToField/#", 347 ACCOUNT_ID_ADD_TO_FIELD); 348 349 /** 350 * THIS URI HAS SPECIAL SEMANTICS 351 * ITS USE IS INTENDED FOR THE UI APPLICATION TO MARK CHANGES THAT NEED TO BE SYNCED BACK 352 * TO A SERVER VIA A SYNC ADAPTER 353 */ 354 matcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); 355 356 /** 357 * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY 358 * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI 359 * BY THE UI APPLICATION 360 */ 361 // All deleted messages 362 matcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE); 363 // A specific deleted message 364 matcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID); 365 366 // All updated messages 367 matcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE); 368 // A specific updated message 369 matcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID); 370 371 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues(); 372 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0); 373 374 matcher.addURI(EmailContent.AUTHORITY, "policy", POLICY); 375 matcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID); 376 377 // All quick responses 378 matcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE); 379 // A specific quick response 380 matcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID); 381 // All quick responses associated with a particular account id 382 matcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#", 383 QUICK_RESPONSE_ACCOUNT_ID); 384 385 matcher.addURI(EmailContent.AUTHORITY, "uifolders/*", UI_FOLDERS); 386 matcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES); 387 matcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE); 388 matcher.addURI(EmailContent.AUTHORITY, "uisendmail/*", UI_SENDMAIL); 389 matcher.addURI(EmailContent.AUTHORITY, "uiundo/*", UI_UNDO); 390 matcher.addURI(EmailContent.AUTHORITY, "uisavedraft/*", UI_SAVEDRAFT); 391 matcher.addURI(EmailContent.AUTHORITY, "uiupdatedraft/#", UI_UPDATEDRAFT); 392 matcher.addURI(EmailContent.AUTHORITY, "uisenddraft/#", UI_SENDDRAFT); 393 } 394 395 /** 396 * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in 397 * @param uri the Uri to match 398 * @return the match value 399 */ 400 private static int findMatch(Uri uri, String methodName) { 401 int match = sURIMatcher.match(uri); 402 if (match < 0) { 403 throw new IllegalArgumentException("Unknown uri: " + uri); 404 } else if (Logging.LOGD) { 405 Log.v(TAG, methodName + ": uri=" + uri + ", match is " + match); 406 } 407 return match; 408 } 409 410 private SQLiteDatabase mDatabase; 411 private SQLiteDatabase mBodyDatabase; 412 413 /** 414 * Orphan record deletion utility. Generates a sqlite statement like: 415 * delete from <table> where <column> not in (select <foreignColumn> from <foreignTable>) 416 * @param db the EmailProvider database 417 * @param table the table whose orphans are to be removed 418 * @param column the column deletion will be based on 419 * @param foreignColumn the column in the foreign table whose absence will trigger the deletion 420 * @param foreignTable the foreign table 421 */ 422 @VisibleForTesting 423 void deleteUnlinked(SQLiteDatabase db, String table, String column, String foreignColumn, 424 String foreignTable) { 425 int count = db.delete(table, column + " not in (select " + foreignColumn + " from " + 426 foreignTable + ")", null); 427 if (count > 0) { 428 Log.w(TAG, "Found " + count + " orphaned row(s) in " + table); 429 } 430 } 431 432 @VisibleForTesting 433 synchronized SQLiteDatabase getDatabase(Context context) { 434 // Always return the cached database, if we've got one 435 if (mDatabase != null) { 436 return mDatabase; 437 } 438 439 // Whenever we create or re-cache the databases, make sure that we haven't lost one 440 // to corruption 441 checkDatabases(); 442 443 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); 444 mDatabase = helper.getWritableDatabase(); 445 DBHelper.BodyDatabaseHelper bodyHelper = 446 new DBHelper.BodyDatabaseHelper(context, BODY_DATABASE_NAME); 447 mBodyDatabase = bodyHelper.getWritableDatabase(); 448 if (mBodyDatabase != null) { 449 String bodyFileName = mBodyDatabase.getPath(); 450 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); 451 } 452 453 // Restore accounts if the database is corrupted... 454 restoreIfNeeded(context, mDatabase); 455 456 if (Email.DEBUG) { 457 Log.d(TAG, "Deleting orphans..."); 458 } 459 // Check for any orphaned Messages in the updated/deleted tables 460 deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME); 461 deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME); 462 // Delete orphaned mailboxes/messages/policies (account no longer exists) 463 deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY, AccountColumns.ID, 464 Account.TABLE_NAME); 465 deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY, AccountColumns.ID, 466 Account.TABLE_NAME); 467 deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns.ID, AccountColumns.POLICY_KEY, 468 Account.TABLE_NAME); 469 470 if (Email.DEBUG) { 471 Log.d(TAG, "EmailProvider pre-caching..."); 472 } 473 preCacheData(); 474 if (Email.DEBUG) { 475 Log.d(TAG, "EmailProvider ready."); 476 } 477 return mDatabase; 478 } 479 480 /** 481 * Pre-cache all of the items in a given table meeting the selection criteria 482 * @param tableUri the table uri 483 * @param baseProjection the base projection of that table 484 * @param selection the selection criteria 485 */ 486 private void preCacheTable(Uri tableUri, String[] baseProjection, String selection) { 487 Cursor c = query(tableUri, EmailContent.ID_PROJECTION, selection, null, null); 488 try { 489 while (c.moveToNext()) { 490 long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 491 Cursor cachedCursor = query(ContentUris.withAppendedId( 492 tableUri, id), baseProjection, null, null, null); 493 if (cachedCursor != null) { 494 // For accounts, create a mailbox type map entry (if necessary) 495 if (tableUri == Account.CONTENT_URI) { 496 getOrCreateAccountMailboxTypeMap(id); 497 } 498 cachedCursor.close(); 499 } 500 } 501 } finally { 502 c.close(); 503 } 504 } 505 506 private final HashMap<Long, HashMap<Integer, Long>> mMailboxTypeMap = 507 new HashMap<Long, HashMap<Integer, Long>>(); 508 509 private HashMap<Integer, Long> getOrCreateAccountMailboxTypeMap(long accountId) { 510 synchronized(mMailboxTypeMap) { 511 HashMap<Integer, Long> accountMailboxTypeMap = mMailboxTypeMap.get(accountId); 512 if (accountMailboxTypeMap == null) { 513 accountMailboxTypeMap = new HashMap<Integer, Long>(); 514 mMailboxTypeMap.put(accountId, accountMailboxTypeMap); 515 } 516 return accountMailboxTypeMap; 517 } 518 } 519 520 private void addToMailboxTypeMap(Cursor c) { 521 long accountId = c.getLong(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN); 522 int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 523 synchronized(mMailboxTypeMap) { 524 HashMap<Integer, Long> accountMailboxTypeMap = 525 getOrCreateAccountMailboxTypeMap(accountId); 526 accountMailboxTypeMap.put(type, c.getLong(Mailbox.CONTENT_ID_COLUMN)); 527 } 528 } 529 530 private long getMailboxIdFromMailboxTypeMap(long accountId, int type) { 531 synchronized(mMailboxTypeMap) { 532 HashMap<Integer, Long> accountMap = mMailboxTypeMap.get(accountId); 533 Long mailboxId = null; 534 if (accountMap != null) { 535 mailboxId = accountMap.get(type); 536 } 537 if (mailboxId == null) return Mailbox.NO_MAILBOX; 538 return mailboxId; 539 } 540 } 541 542 private void preCacheData() { 543 synchronized(mMailboxTypeMap) { 544 mMailboxTypeMap.clear(); 545 546 // Pre-cache accounts, host auth's, policies, and special mailboxes 547 preCacheTable(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null); 548 preCacheTable(HostAuth.CONTENT_URI, HostAuth.CONTENT_PROJECTION, null); 549 preCacheTable(Policy.CONTENT_URI, Policy.CONTENT_PROJECTION, null); 550 preCacheTable(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 551 MAILBOX_PRE_CACHE_SELECTION); 552 553 // Create a map from account,type to a mailbox 554 Map<String, Cursor> snapshot = mCacheMailbox.getSnapshot(); 555 Collection<Cursor> values = snapshot.values(); 556 if (values != null) { 557 for (Cursor c: values) { 558 if (c.moveToFirst()) { 559 addToMailboxTypeMap(c); 560 } 561 } 562 } 563 } 564 } 565 566 /*package*/ static SQLiteDatabase getReadableDatabase(Context context) { 567 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); 568 return helper.getReadableDatabase(); 569 } 570 571 /** 572 * Restore user Account and HostAuth data from our backup database 573 */ 574 public static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) { 575 if (Email.DEBUG) { 576 Log.w(TAG, "restoreIfNeeded..."); 577 } 578 // Check for legacy backup 579 String legacyBackup = Preferences.getLegacyBackupPreference(context); 580 // If there's a legacy backup, create a new-style backup and delete the legacy backup 581 // In the 1:1000000000 chance that the user gets an app update just as his database becomes 582 // corrupt, oh well... 583 if (!TextUtils.isEmpty(legacyBackup)) { 584 backupAccounts(context, mainDatabase); 585 Preferences.clearLegacyBackupPreference(context); 586 Log.w(TAG, "Created new EmailProvider backup database"); 587 return; 588 } 589 590 // If we have accounts, we're done 591 Cursor c = mainDatabase.query(Account.TABLE_NAME, EmailContent.ID_PROJECTION, null, null, 592 null, null, null); 593 if (c.moveToFirst()) { 594 if (Email.DEBUG) { 595 Log.w(TAG, "restoreIfNeeded: Account exists."); 596 } 597 return; // At least one account exists. 598 } 599 restoreAccounts(context, mainDatabase); 600 } 601 602 /** {@inheritDoc} */ 603 @Override 604 public void shutdown() { 605 if (mDatabase != null) { 606 mDatabase.close(); 607 mDatabase = null; 608 } 609 if (mBodyDatabase != null) { 610 mBodyDatabase.close(); 611 mBodyDatabase = null; 612 } 613 } 614 615 /*package*/ static void deleteMessageOrphans(SQLiteDatabase database, String tableName) { 616 if (database != null) { 617 // We'll look at all of the items in the table; there won't be many typically 618 Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); 619 // Usually, there will be nothing in these tables, so make a quick check 620 try { 621 if (c.getCount() == 0) return; 622 ArrayList<Long> foundMailboxes = new ArrayList<Long>(); 623 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>(); 624 ArrayList<Long> deleteList = new ArrayList<Long>(); 625 String[] bindArray = new String[1]; 626 while (c.moveToNext()) { 627 // Get the mailbox key and see if we've already found this mailbox 628 // If so, we're fine 629 long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY); 630 // If we already know this mailbox doesn't exist, mark the message for deletion 631 if (notFoundMailboxes.contains(mailboxId)) { 632 deleteList.add(c.getLong(ORPHANS_ID)); 633 // If we don't know about this mailbox, we'll try to find it 634 } else if (!foundMailboxes.contains(mailboxId)) { 635 bindArray[0] = Long.toString(mailboxId); 636 Cursor boxCursor = database.query(Mailbox.TABLE_NAME, 637 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null); 638 try { 639 // If it exists, we'll add it to the "found" mailboxes 640 if (boxCursor.moveToFirst()) { 641 foundMailboxes.add(mailboxId); 642 // Otherwise, we'll add to "not found" and mark the message for deletion 643 } else { 644 notFoundMailboxes.add(mailboxId); 645 deleteList.add(c.getLong(ORPHANS_ID)); 646 } 647 } finally { 648 boxCursor.close(); 649 } 650 } 651 } 652 // Now, delete the orphan messages 653 for (long messageId: deleteList) { 654 bindArray[0] = Long.toString(messageId); 655 database.delete(tableName, WHERE_ID, bindArray); 656 } 657 } finally { 658 c.close(); 659 } 660 } 661 } 662 663 @Override 664 public int delete(Uri uri, String selection, String[] selectionArgs) { 665 final int match = findMatch(uri, "delete"); 666 Context context = getContext(); 667 // Pick the correct database for this operation 668 // If we're in a transaction already (which would happen during applyBatch), then the 669 // body database is already attached to the email database and any attempt to use the 670 // body database directly will result in a SQLiteException (the database is locked) 671 SQLiteDatabase db = getDatabase(context); 672 int table = match >> BASE_SHIFT; 673 String id = "0"; 674 boolean messageDeletion = false; 675 ContentResolver resolver = context.getContentResolver(); 676 677 ContentCache cache = mContentCaches[table]; 678 String tableName = TABLE_NAMES[table]; 679 int result = -1; 680 681 try { 682 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 683 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 684 Log.d(TAG, "Notify UIProvider of delete"); 685 resolver.notifyChange(UIPROVIDER_MESSAGE_NOTIFIER, null); 686 } 687 } 688 switch (match) { 689 case UI_MESSAGE: 690 return uiDeleteMessage(uri); 691 // These are cases in which one or more Messages might get deleted, either by 692 // cascade or explicitly 693 case MAILBOX_ID: 694 case MAILBOX: 695 case ACCOUNT_ID: 696 case ACCOUNT: 697 case MESSAGE: 698 case SYNCED_MESSAGE_ID: 699 case MESSAGE_ID: 700 // Handle lost Body records here, since this cannot be done in a trigger 701 // The process is: 702 // 1) Begin a transaction, ensuring that both databases are affected atomically 703 // 2) Do the requested deletion, with cascading deletions handled in triggers 704 // 3) End the transaction, committing all changes atomically 705 // 706 // Bodies are auto-deleted here; Attachments are auto-deleted via trigger 707 messageDeletion = true; 708 db.beginTransaction(); 709 break; 710 } 711 switch (match) { 712 case BODY_ID: 713 case DELETED_MESSAGE_ID: 714 case SYNCED_MESSAGE_ID: 715 case MESSAGE_ID: 716 case UPDATED_MESSAGE_ID: 717 case ATTACHMENT_ID: 718 case MAILBOX_ID: 719 case ACCOUNT_ID: 720 case HOSTAUTH_ID: 721 case POLICY_ID: 722 case QUICK_RESPONSE_ID: 723 id = uri.getPathSegments().get(1); 724 if (match == SYNCED_MESSAGE_ID) { 725 // For synced messages, first copy the old message to the deleted table and 726 // delete it from the updated table (in case it was updated first) 727 // Note that this is all within a transaction, for atomicity 728 db.execSQL(DELETED_MESSAGE_INSERT + id); 729 db.execSQL(UPDATED_MESSAGE_DELETE + id); 730 } 731 if (cache != null) { 732 cache.lock(id); 733 } 734 try { 735 result = db.delete(tableName, whereWithId(id, selection), selectionArgs); 736 if (cache != null) { 737 switch(match) { 738 case ACCOUNT_ID: 739 // Account deletion will clear all of the caches, as HostAuth's, 740 // Mailboxes, and Messages will be deleted in the process 741 mCacheMailbox.invalidate("Delete", uri, selection); 742 mCacheHostAuth.invalidate("Delete", uri, selection); 743 mCachePolicy.invalidate("Delete", uri, selection); 744 //$FALL-THROUGH$ 745 case MAILBOX_ID: 746 // Mailbox deletion will clear the Message cache 747 mCacheMessage.invalidate("Delete", uri, selection); 748 //$FALL-THROUGH$ 749 case SYNCED_MESSAGE_ID: 750 case MESSAGE_ID: 751 case HOSTAUTH_ID: 752 case POLICY_ID: 753 cache.invalidate("Delete", uri, selection); 754 // Make sure all data is properly cached 755 if (match != MESSAGE_ID) { 756 preCacheData(); 757 } 758 break; 759 } 760 } 761 } finally { 762 if (cache != null) { 763 cache.unlock(id); 764 } 765 } 766 break; 767 case ATTACHMENTS_MESSAGE_ID: 768 // All attachments for the given message 769 id = uri.getPathSegments().get(2); 770 result = db.delete(tableName, 771 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs); 772 break; 773 774 case BODY: 775 case MESSAGE: 776 case DELETED_MESSAGE: 777 case UPDATED_MESSAGE: 778 case ATTACHMENT: 779 case MAILBOX: 780 case ACCOUNT: 781 case HOSTAUTH: 782 case POLICY: 783 switch(match) { 784 // See the comments above for deletion of ACCOUNT_ID, etc 785 case ACCOUNT: 786 mCacheMailbox.invalidate("Delete", uri, selection); 787 mCacheHostAuth.invalidate("Delete", uri, selection); 788 mCachePolicy.invalidate("Delete", uri, selection); 789 //$FALL-THROUGH$ 790 case MAILBOX: 791 mCacheMessage.invalidate("Delete", uri, selection); 792 //$FALL-THROUGH$ 793 case MESSAGE: 794 case HOSTAUTH: 795 case POLICY: 796 cache.invalidate("Delete", uri, selection); 797 break; 798 } 799 result = db.delete(tableName, selection, selectionArgs); 800 switch(match) { 801 case ACCOUNT: 802 case MAILBOX: 803 case HOSTAUTH: 804 case POLICY: 805 // Make sure all data is properly cached 806 preCacheData(); 807 break; 808 } 809 break; 810 811 default: 812 throw new IllegalArgumentException("Unknown URI " + uri); 813 } 814 if (messageDeletion) { 815 if (match == MESSAGE_ID) { 816 // Delete the Body record associated with the deleted message 817 db.execSQL(DELETE_BODY + id); 818 } else { 819 // Delete any orphaned Body records 820 db.execSQL(DELETE_ORPHAN_BODIES); 821 } 822 db.setTransactionSuccessful(); 823 } 824 } catch (SQLiteException e) { 825 checkDatabases(); 826 throw e; 827 } finally { 828 if (messageDeletion) { 829 db.endTransaction(); 830 } 831 } 832 833 // Notify all notifier cursors 834 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id); 835 836 // Notify all email content cursors 837 resolver.notifyChange(EmailContent.CONTENT_URI, null); 838 return result; 839 } 840 841 @Override 842 // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) 843 public String getType(Uri uri) { 844 int match = findMatch(uri, "getType"); 845 switch (match) { 846 case BODY_ID: 847 return "vnd.android.cursor.item/email-body"; 848 case BODY: 849 return "vnd.android.cursor.dir/email-body"; 850 case UPDATED_MESSAGE_ID: 851 case MESSAGE_ID: 852 // NOTE: According to the framework folks, we're supposed to invent mime types as 853 // a way of passing information to drag & drop recipients. 854 // If there's a mailboxId parameter in the url, we respond with a mime type that 855 // has -n appended, where n is the mailboxId of the message. The drag & drop code 856 // uses this information to know not to allow dragging the item to its own mailbox 857 String mimeType = EMAIL_MESSAGE_MIME_TYPE; 858 String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID); 859 if (mailboxId != null) { 860 mimeType += "-" + mailboxId; 861 } 862 return mimeType; 863 case UPDATED_MESSAGE: 864 case MESSAGE: 865 return "vnd.android.cursor.dir/email-message"; 866 case MAILBOX: 867 return "vnd.android.cursor.dir/email-mailbox"; 868 case MAILBOX_ID: 869 return "vnd.android.cursor.item/email-mailbox"; 870 case ACCOUNT: 871 return "vnd.android.cursor.dir/email-account"; 872 case ACCOUNT_ID: 873 return "vnd.android.cursor.item/email-account"; 874 case ATTACHMENTS_MESSAGE_ID: 875 case ATTACHMENT: 876 return "vnd.android.cursor.dir/email-attachment"; 877 case ATTACHMENT_ID: 878 return EMAIL_ATTACHMENT_MIME_TYPE; 879 case HOSTAUTH: 880 return "vnd.android.cursor.dir/email-hostauth"; 881 case HOSTAUTH_ID: 882 return "vnd.android.cursor.item/email-hostauth"; 883 default: 884 throw new IllegalArgumentException("Unknown URI " + uri); 885 } 886 } 887 888 private static final Uri UIPROVIDER_MESSAGE_NOTIFIER = 889 Uri.parse("content://" + UIProvider.AUTHORITY + "/uimessages"); 890 891 @Override 892 public Uri insert(Uri uri, ContentValues values) { 893 int match = findMatch(uri, "insert"); 894 Context context = getContext(); 895 ContentResolver resolver = context.getContentResolver(); 896 897 // See the comment at delete(), above 898 SQLiteDatabase db = getDatabase(context); 899 int table = match >> BASE_SHIFT; 900 String id = "0"; 901 long longId; 902 903 // We do NOT allow setting of unreadCount/messageCount via the provider 904 // These columns are maintained via triggers 905 if (match == MAILBOX_ID || match == MAILBOX) { 906 values.put(MailboxColumns.UNREAD_COUNT, 0); 907 values.put(MailboxColumns.MESSAGE_COUNT, 0); 908 } 909 910 Uri resultUri = null; 911 912 try { 913 switch (match) { 914 case UI_SAVEDRAFT: 915 return uiSaveDraft(uri, values); 916 case UI_SENDMAIL: 917 return uiSendMail(uri, values); 918 // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE 919 // or DELETED_MESSAGE; see the comment below for details 920 case UPDATED_MESSAGE: 921 case DELETED_MESSAGE: 922 case MESSAGE: 923 case BODY: 924 case ATTACHMENT: 925 case MAILBOX: 926 case ACCOUNT: 927 case HOSTAUTH: 928 case POLICY: 929 case QUICK_RESPONSE: 930 longId = db.insert(TABLE_NAMES[table], "foo", values); 931 resultUri = ContentUris.withAppendedId(uri, longId); 932 switch(match) { 933 case MESSAGE: 934 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 935 Log.d(TAG, "Notify UIProvider of insert"); 936 resolver.notifyChange(UIPROVIDER_MESSAGE_NOTIFIER, null); 937 } 938 break; 939 case MAILBOX: 940 if (values.containsKey(MailboxColumns.TYPE)) { 941 // Only cache special mailbox types 942 int type = values.getAsInteger(MailboxColumns.TYPE); 943 if (type != Mailbox.TYPE_INBOX && type != Mailbox.TYPE_OUTBOX && 944 type != Mailbox.TYPE_DRAFTS && type != Mailbox.TYPE_SENT && 945 type != Mailbox.TYPE_TRASH && type != Mailbox.TYPE_SEARCH) { 946 break; 947 } 948 } 949 //$FALL-THROUGH$ 950 case ACCOUNT: 951 case HOSTAUTH: 952 case POLICY: 953 // Cache new account, host auth, policy, and some mailbox rows 954 Cursor c = query(resultUri, CACHE_PROJECTIONS[table], null, null, null); 955 if (c != null) { 956 if (match == MAILBOX) { 957 addToMailboxTypeMap(c); 958 } else if (match == ACCOUNT) { 959 getOrCreateAccountMailboxTypeMap(longId); 960 } 961 c.close(); 962 } 963 break; 964 } 965 // Clients shouldn't normally be adding rows to these tables, as they are 966 // maintained by triggers. However, we need to be able to do this for unit 967 // testing, so we allow the insert and then throw the same exception that we 968 // would if this weren't allowed. 969 if (match == UPDATED_MESSAGE || match == DELETED_MESSAGE) { 970 throw new IllegalArgumentException("Unknown URL " + uri); 971 } 972 if (match == ATTACHMENT) { 973 int flags = 0; 974 if (values.containsKey(Attachment.FLAGS)) { 975 flags = values.getAsInteger(Attachment.FLAGS); 976 } 977 // Report all new attachments to the download service 978 mAttachmentService.attachmentChanged(getContext(), longId, flags); 979 } 980 break; 981 case MAILBOX_ID: 982 // This implies adding a message to a mailbox 983 // Hmm, a problem here is that we can't link the account as well, so it must be 984 // already in the values... 985 longId = Long.parseLong(uri.getPathSegments().get(1)); 986 values.put(MessageColumns.MAILBOX_KEY, longId); 987 return insert(Message.CONTENT_URI, values); // Recurse 988 case MESSAGE_ID: 989 // This implies adding an attachment to a message. 990 id = uri.getPathSegments().get(1); 991 longId = Long.parseLong(id); 992 values.put(AttachmentColumns.MESSAGE_KEY, longId); 993 return insert(Attachment.CONTENT_URI, values); // Recurse 994 case ACCOUNT_ID: 995 // This implies adding a mailbox to an account. 996 longId = Long.parseLong(uri.getPathSegments().get(1)); 997 values.put(MailboxColumns.ACCOUNT_KEY, longId); 998 return insert(Mailbox.CONTENT_URI, values); // Recurse 999 case ATTACHMENTS_MESSAGE_ID: 1000 longId = db.insert(TABLE_NAMES[table], "foo", values); 1001 resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId); 1002 break; 1003 default: 1004 throw new IllegalArgumentException("Unknown URL " + uri); 1005 } 1006 } catch (SQLiteException e) { 1007 checkDatabases(); 1008 throw e; 1009 } 1010 1011 // Notify all notifier cursors 1012 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id); 1013 1014 // Notify all existing cursors. 1015 resolver.notifyChange(EmailContent.CONTENT_URI, null); 1016 return resultUri; 1017 } 1018 1019 @Override 1020 public boolean onCreate() { 1021 checkDatabases(); 1022 return false; 1023 } 1024 1025 /** 1026 * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must 1027 * always be in sync (i.e. there are two database or NO databases). This code will delete 1028 * any "orphan" database, so that both will be created together. Note that an "orphan" database 1029 * will exist after either of the individual databases is deleted due to data corruption. 1030 */ 1031 public void checkDatabases() { 1032 // Uncache the databases 1033 if (mDatabase != null) { 1034 mDatabase = null; 1035 } 1036 if (mBodyDatabase != null) { 1037 mBodyDatabase = null; 1038 } 1039 // Look for orphans, and delete as necessary; these must always be in sync 1040 File databaseFile = getContext().getDatabasePath(DATABASE_NAME); 1041 File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME); 1042 1043 // TODO Make sure attachments are deleted 1044 if (databaseFile.exists() && !bodyFile.exists()) { 1045 Log.w(TAG, "Deleting orphaned EmailProvider database..."); 1046 databaseFile.delete(); 1047 } else if (bodyFile.exists() && !databaseFile.exists()) { 1048 Log.w(TAG, "Deleting orphaned EmailProviderBody database..."); 1049 bodyFile.delete(); 1050 } 1051 } 1052 @Override 1053 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1054 String sortOrder) { 1055 long time = 0L; 1056 if (Email.DEBUG) { 1057 time = System.nanoTime(); 1058 } 1059 Cursor c = null; 1060 int match; 1061 try { 1062 match = findMatch(uri, "query"); 1063 } catch (IllegalArgumentException e) { 1064 String uriString = uri.toString(); 1065 // If we were passed an illegal uri, see if it ends in /-1 1066 // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor 1067 if (uriString != null && uriString.endsWith("/-1")) { 1068 uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0"); 1069 match = findMatch(uri, "query"); 1070 switch (match) { 1071 case BODY_ID: 1072 case MESSAGE_ID: 1073 case DELETED_MESSAGE_ID: 1074 case UPDATED_MESSAGE_ID: 1075 case ATTACHMENT_ID: 1076 case MAILBOX_ID: 1077 case ACCOUNT_ID: 1078 case HOSTAUTH_ID: 1079 case POLICY_ID: 1080 return new MatrixCursor(projection, 0); 1081 } 1082 } 1083 throw e; 1084 } 1085 Context context = getContext(); 1086 // See the comment at delete(), above 1087 SQLiteDatabase db = getDatabase(context); 1088 int table = match >> BASE_SHIFT; 1089 String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT); 1090 String id; 1091 1092 // Find the cache for this query's table (if any) 1093 ContentCache cache = null; 1094 String tableName = TABLE_NAMES[table]; 1095 // We can only use the cache if there's no selection 1096 if (selection == null) { 1097 cache = mContentCaches[table]; 1098 } 1099 if (cache == null) { 1100 ContentCache.notCacheable(uri, selection); 1101 } 1102 1103 try { 1104 switch (match) { 1105 // First, dispatch queries from UnfiedEmail 1106 case UI_UNDO: 1107 return uiUndo(uri, projection); 1108 case UI_FOLDERS: 1109 case UI_MESSAGES: 1110 case UI_MESSAGE: 1111 // For now, we don't allow selection criteria within these queries 1112 if (selection != null || selectionArgs != null) { 1113 throw new IllegalArgumentException("UI queries can't have selection/args"); 1114 } 1115 c = uiQuery(match, uri, projection); 1116 return c; 1117 case ACCOUNT_DEFAULT_ID: 1118 // Start with a snapshot of the cache 1119 Map<String, Cursor> accountCache = mCacheAccount.getSnapshot(); 1120 long accountId = Account.NO_ACCOUNT; 1121 // Find the account with "isDefault" set, or the lowest account ID otherwise. 1122 // Note that the snapshot from the cached isn't guaranteed to be sorted in any 1123 // way. 1124 Collection<Cursor> accounts = accountCache.values(); 1125 for (Cursor accountCursor: accounts) { 1126 // For now, at least, we can have zero count cursors (e.g. if someone looks 1127 // up a non-existent id); we need to skip these 1128 if (accountCursor.moveToFirst()) { 1129 boolean isDefault = 1130 accountCursor.getInt(Account.CONTENT_IS_DEFAULT_COLUMN) == 1; 1131 long iterId = accountCursor.getLong(Account.CONTENT_ID_COLUMN); 1132 // We'll remember this one if it's the default or the first one we see 1133 if (isDefault) { 1134 accountId = iterId; 1135 break; 1136 } else if ((accountId == Account.NO_ACCOUNT) || (iterId < accountId)) { 1137 accountId = iterId; 1138 } 1139 } 1140 } 1141 // Return a cursor with an id projection 1142 MatrixCursor mc = new MatrixCursor(EmailContent.ID_PROJECTION); 1143 mc.addRow(new Object[] {accountId}); 1144 c = mc; 1145 break; 1146 case MAILBOX_ID_FROM_ACCOUNT_AND_TYPE: 1147 // Get accountId and type and find the mailbox in our map 1148 List<String> pathSegments = uri.getPathSegments(); 1149 accountId = Long.parseLong(pathSegments.get(1)); 1150 int type = Integer.parseInt(pathSegments.get(2)); 1151 long mailboxId = getMailboxIdFromMailboxTypeMap(accountId, type); 1152 // Return a cursor with an id projection 1153 mc = new MatrixCursor(EmailContent.ID_PROJECTION); 1154 mc.addRow(new Object[] {mailboxId}); 1155 c = mc; 1156 break; 1157 case BODY: 1158 case MESSAGE: 1159 case UPDATED_MESSAGE: 1160 case DELETED_MESSAGE: 1161 case ATTACHMENT: 1162 case MAILBOX: 1163 case ACCOUNT: 1164 case HOSTAUTH: 1165 case POLICY: 1166 case QUICK_RESPONSE: 1167 // Special-case "count of accounts"; it's common and we always know it 1168 if (match == ACCOUNT && Arrays.equals(projection, EmailContent.COUNT_COLUMNS) && 1169 selection == null && limit.equals("1")) { 1170 int accountCount = mMailboxTypeMap.size(); 1171 // In the rare case there are MAX_CACHED_ACCOUNTS or more, we can't do this 1172 if (accountCount < MAX_CACHED_ACCOUNTS) { 1173 mc = new MatrixCursor(projection, 1); 1174 mc.addRow(new Object[] {accountCount}); 1175 c = mc; 1176 break; 1177 } 1178 } 1179 c = db.query(tableName, projection, 1180 selection, selectionArgs, null, null, sortOrder, limit); 1181 break; 1182 case BODY_ID: 1183 case MESSAGE_ID: 1184 case DELETED_MESSAGE_ID: 1185 case UPDATED_MESSAGE_ID: 1186 case ATTACHMENT_ID: 1187 case MAILBOX_ID: 1188 case ACCOUNT_ID: 1189 case HOSTAUTH_ID: 1190 case POLICY_ID: 1191 case QUICK_RESPONSE_ID: 1192 id = uri.getPathSegments().get(1); 1193 if (cache != null) { 1194 c = cache.getCachedCursor(id, projection); 1195 } 1196 if (c == null) { 1197 CacheToken token = null; 1198 if (cache != null) { 1199 token = cache.getCacheToken(id); 1200 } 1201 c = db.query(tableName, projection, whereWithId(id, selection), 1202 selectionArgs, null, null, sortOrder, limit); 1203 if (cache != null) { 1204 c = cache.putCursor(c, id, projection, token); 1205 } 1206 } 1207 break; 1208 case ATTACHMENTS_MESSAGE_ID: 1209 // All attachments for the given message 1210 id = uri.getPathSegments().get(2); 1211 c = db.query(Attachment.TABLE_NAME, projection, 1212 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), 1213 selectionArgs, null, null, sortOrder, limit); 1214 break; 1215 case QUICK_RESPONSE_ACCOUNT_ID: 1216 // All quick responses for the given account 1217 id = uri.getPathSegments().get(2); 1218 c = db.query(QuickResponse.TABLE_NAME, projection, 1219 whereWith(QuickResponse.ACCOUNT_KEY + "=" + id, selection), 1220 selectionArgs, null, null, sortOrder); 1221 break; 1222 default: 1223 throw new IllegalArgumentException("Unknown URI " + uri); 1224 } 1225 } catch (SQLiteException e) { 1226 checkDatabases(); 1227 throw e; 1228 } catch (RuntimeException e) { 1229 checkDatabases(); 1230 e.printStackTrace(); 1231 throw e; 1232 } finally { 1233 if (cache != null && c != null && Email.DEBUG) { 1234 cache.recordQueryTime(c, System.nanoTime() - time); 1235 } 1236 if (c == null) { 1237 // This should never happen, but let's be sure to log it... 1238 Log.e(TAG, "Query returning null for uri: " + uri + ", selection: " + selection); 1239 } 1240 } 1241 1242 if ((c != null) && !isTemporary()) { 1243 c.setNotificationUri(getContext().getContentResolver(), uri); 1244 } 1245 return c; 1246 } 1247 1248 private String whereWithId(String id, String selection) { 1249 StringBuilder sb = new StringBuilder(256); 1250 sb.append("_id="); 1251 sb.append(id); 1252 if (selection != null) { 1253 sb.append(" AND ("); 1254 sb.append(selection); 1255 sb.append(')'); 1256 } 1257 return sb.toString(); 1258 } 1259 1260 /** 1261 * Combine a locally-generated selection with a user-provided selection 1262 * 1263 * This introduces risk that the local selection might insert incorrect chars 1264 * into the SQL, so use caution. 1265 * 1266 * @param where locally-generated selection, must not be null 1267 * @param selection user-provided selection, may be null 1268 * @return a single selection string 1269 */ 1270 private String whereWith(String where, String selection) { 1271 if (selection == null) { 1272 return where; 1273 } 1274 StringBuilder sb = new StringBuilder(where); 1275 sb.append(" AND ("); 1276 sb.append(selection); 1277 sb.append(')'); 1278 1279 return sb.toString(); 1280 } 1281 1282 /** 1283 * Restore a HostAuth from a database, given its unique id 1284 * @param db the database 1285 * @param id the unique id (_id) of the row 1286 * @return a fully populated HostAuth or null if the row does not exist 1287 */ 1288 private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) { 1289 Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION, 1290 HostAuth.RECORD_ID + "=?", new String[] {Long.toString(id)}, null, null, null); 1291 try { 1292 if (c.moveToFirst()) { 1293 HostAuth hostAuth = new HostAuth(); 1294 hostAuth.restore(c); 1295 return hostAuth; 1296 } 1297 return null; 1298 } finally { 1299 c.close(); 1300 } 1301 } 1302 1303 /** 1304 * Copy the Account and HostAuth tables from one database to another 1305 * @param fromDatabase the source database 1306 * @param toDatabase the destination database 1307 * @return the number of accounts copied, or -1 if an error occurred 1308 */ 1309 private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) { 1310 if (fromDatabase == null || toDatabase == null) return -1; 1311 int copyCount = 0; 1312 try { 1313 // Lock both databases; for the "from" database, we don't want anyone changing it from 1314 // under us; for the "to" database, we want to make the operation atomic 1315 fromDatabase.beginTransaction(); 1316 toDatabase.beginTransaction(); 1317 // Delete anything hanging around here 1318 toDatabase.delete(Account.TABLE_NAME, null, null); 1319 toDatabase.delete(HostAuth.TABLE_NAME, null, null); 1320 // Get our account cursor 1321 Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION, 1322 null, null, null, null, null); 1323 boolean noErrors = true; 1324 try { 1325 // Loop through accounts, copying them and associated host auth's 1326 while (c.moveToNext()) { 1327 Account account = new Account(); 1328 account.restore(c); 1329 1330 // Clear security sync key and sync key, as these were specific to the state of 1331 // the account, and we've reset that... 1332 // Clear policy key so that we can re-establish policies from the server 1333 // TODO This is pretty EAS specific, but there's a lot of that around 1334 account.mSecuritySyncKey = null; 1335 account.mSyncKey = null; 1336 account.mPolicyKey = 0; 1337 1338 // Copy host auth's and update foreign keys 1339 HostAuth hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeyRecv); 1340 // The account might have gone away, though very unlikely 1341 if (hostAuth == null) continue; 1342 account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null, 1343 hostAuth.toContentValues()); 1344 // EAS accounts have no send HostAuth 1345 if (account.mHostAuthKeySend > 0) { 1346 hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend); 1347 // Belt and suspenders; I can't imagine that this is possible, since we 1348 // checked the validity of the account above, and the database is now locked 1349 if (hostAuth == null) continue; 1350 account.mHostAuthKeySend = toDatabase.insert(HostAuth.TABLE_NAME, null, 1351 hostAuth.toContentValues()); 1352 } 1353 // Now, create the account in the "to" database 1354 toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues()); 1355 copyCount++; 1356 } 1357 } catch (SQLiteException e) { 1358 noErrors = false; 1359 copyCount = -1; 1360 } finally { 1361 fromDatabase.endTransaction(); 1362 if (noErrors) { 1363 // Say it's ok to commit 1364 toDatabase.setTransactionSuccessful(); 1365 } 1366 toDatabase.endTransaction(); 1367 c.close(); 1368 } 1369 } catch (SQLiteException e) { 1370 copyCount = -1; 1371 } 1372 return copyCount; 1373 } 1374 1375 private static SQLiteDatabase getBackupDatabase(Context context) { 1376 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, BACKUP_DATABASE_NAME); 1377 return helper.getWritableDatabase(); 1378 } 1379 1380 /** 1381 * Backup account data, returning the number of accounts backed up 1382 */ 1383 private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) { 1384 if (Email.DEBUG) { 1385 Log.d(TAG, "backupAccounts..."); 1386 } 1387 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1388 try { 1389 int numBackedUp = copyAccountTables(mainDatabase, backupDatabase); 1390 if (numBackedUp < 0) { 1391 Log.e(TAG, "Account backup failed!"); 1392 } else if (Email.DEBUG) { 1393 Log.d(TAG, "Backed up " + numBackedUp + " accounts..."); 1394 } 1395 return numBackedUp; 1396 } finally { 1397 if (backupDatabase != null) { 1398 backupDatabase.close(); 1399 } 1400 } 1401 } 1402 1403 /** 1404 * Restore account data, returning the number of accounts restored 1405 */ 1406 private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) { 1407 if (Email.DEBUG) { 1408 Log.d(TAG, "restoreAccounts..."); 1409 } 1410 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1411 try { 1412 int numRecovered = copyAccountTables(backupDatabase, mainDatabase); 1413 if (numRecovered > 0) { 1414 Log.e(TAG, "Recovered " + numRecovered + " accounts!"); 1415 } else if (numRecovered < 0) { 1416 Log.e(TAG, "Account recovery failed?"); 1417 } else if (Email.DEBUG) { 1418 Log.d(TAG, "No accounts to restore..."); 1419 } 1420 return numRecovered; 1421 } finally { 1422 if (backupDatabase != null) { 1423 backupDatabase.close(); 1424 } 1425 } 1426 } 1427 1428 @Override 1429 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1430 // Handle this special case the fastest possible way 1431 if (uri == INTEGRITY_CHECK_URI) { 1432 checkDatabases(); 1433 return 0; 1434 } else if (uri == ACCOUNT_BACKUP_URI) { 1435 return backupAccounts(getContext(), getDatabase(getContext())); 1436 } 1437 1438 // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID) 1439 Uri notificationUri = EmailContent.CONTENT_URI; 1440 1441 int match = findMatch(uri, "update"); 1442 Context context = getContext(); 1443 ContentResolver resolver = context.getContentResolver(); 1444 // See the comment at delete(), above 1445 SQLiteDatabase db = getDatabase(context); 1446 int table = match >> BASE_SHIFT; 1447 int result; 1448 1449 // We do NOT allow setting of unreadCount/messageCount via the provider 1450 // These columns are maintained via triggers 1451 if (match == MAILBOX_ID || match == MAILBOX) { 1452 values.remove(MailboxColumns.UNREAD_COUNT); 1453 values.remove(MailboxColumns.MESSAGE_COUNT); 1454 } 1455 1456 ContentCache cache = mContentCaches[table]; 1457 String tableName = TABLE_NAMES[table]; 1458 String id = "0"; 1459 1460 try { 1461 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 1462 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 1463 Log.d(TAG, "Notify UIProvider of update"); 1464 resolver.notifyChange(UIPROVIDER_MESSAGE_NOTIFIER, null); 1465 } 1466 } 1467outer: 1468 switch (match) { 1469 case UI_UPDATEDRAFT: 1470 return uiUpdateDraft(uri, values); 1471 case UI_SENDDRAFT: 1472 return uiSendDraft(uri, values); 1473 case UI_MESSAGE: 1474 return uiUpdateMessage(uri, values); 1475 case MAILBOX_ID_ADD_TO_FIELD: 1476 case ACCOUNT_ID_ADD_TO_FIELD: 1477 id = uri.getPathSegments().get(1); 1478 String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME); 1479 Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME); 1480 if (field == null || add == null) { 1481 throw new IllegalArgumentException("No field/add specified " + uri); 1482 } 1483 ContentValues actualValues = new ContentValues(); 1484 if (cache != null) { 1485 cache.lock(id); 1486 } 1487 try { 1488 db.beginTransaction(); 1489 try { 1490 Cursor c = db.query(tableName, 1491 new String[] {EmailContent.RECORD_ID, field}, 1492 whereWithId(id, selection), 1493 selectionArgs, null, null, null); 1494 try { 1495 result = 0; 1496 String[] bind = new String[1]; 1497 if (c.moveToNext()) { 1498 bind[0] = c.getString(0); // _id 1499 long value = c.getLong(1) + add; 1500 actualValues.put(field, value); 1501 result = db.update(tableName, actualValues, ID_EQUALS, bind); 1502 } 1503 db.setTransactionSuccessful(); 1504 } finally { 1505 c.close(); 1506 } 1507 } finally { 1508 db.endTransaction(); 1509 } 1510 } finally { 1511 if (cache != null) { 1512 cache.unlock(id, actualValues); 1513 } 1514 } 1515 break; 1516 case SYNCED_MESSAGE_ID: 1517 case UPDATED_MESSAGE_ID: 1518 case MESSAGE_ID: 1519 case BODY_ID: 1520 case ATTACHMENT_ID: 1521 case MAILBOX_ID: 1522 case ACCOUNT_ID: 1523 case HOSTAUTH_ID: 1524 case QUICK_RESPONSE_ID: 1525 case POLICY_ID: 1526 id = uri.getPathSegments().get(1); 1527 if (cache != null) { 1528 cache.lock(id); 1529 } 1530 try { 1531 if (match == SYNCED_MESSAGE_ID) { 1532 // For synced messages, first copy the old message to the updated table 1533 // Note the insert or ignore semantics, guaranteeing that only the first 1534 // update will be reflected in the updated message table; therefore this 1535 // row will always have the "original" data 1536 db.execSQL(UPDATED_MESSAGE_INSERT + id); 1537 } else if (match == MESSAGE_ID) { 1538 db.execSQL(UPDATED_MESSAGE_DELETE + id); 1539 } 1540 result = db.update(tableName, values, whereWithId(id, selection), 1541 selectionArgs); 1542 } catch (SQLiteException e) { 1543 // Null out values (so they aren't cached) and re-throw 1544 values = null; 1545 throw e; 1546 } finally { 1547 if (cache != null) { 1548 cache.unlock(id, values); 1549 } 1550 } 1551 if (match == ATTACHMENT_ID) { 1552 if (values.containsKey(Attachment.FLAGS)) { 1553 int flags = values.getAsInteger(Attachment.FLAGS); 1554 mAttachmentService.attachmentChanged(getContext(), 1555 Integer.parseInt(id), flags); 1556 } 1557 } 1558 break; 1559 case BODY: 1560 case MESSAGE: 1561 case UPDATED_MESSAGE: 1562 case ATTACHMENT: 1563 case MAILBOX: 1564 case ACCOUNT: 1565 case HOSTAUTH: 1566 case POLICY: 1567 switch(match) { 1568 // To avoid invalidating the cache on updates, we execute them one at a 1569 // time using the XXX_ID uri; these are all executed atomically 1570 case ACCOUNT: 1571 case MAILBOX: 1572 case HOSTAUTH: 1573 case POLICY: 1574 Cursor c = db.query(tableName, EmailContent.ID_PROJECTION, 1575 selection, selectionArgs, null, null, null); 1576 db.beginTransaction(); 1577 result = 0; 1578 try { 1579 while (c.moveToNext()) { 1580 update(ContentUris.withAppendedId( 1581 uri, c.getLong(EmailContent.ID_PROJECTION_COLUMN)), 1582 values, null, null); 1583 result++; 1584 } 1585 db.setTransactionSuccessful(); 1586 } finally { 1587 db.endTransaction(); 1588 c.close(); 1589 } 1590 break outer; 1591 // Any cached table other than those above should be invalidated here 1592 case MESSAGE: 1593 // If we're doing some generic update, the whole cache needs to be 1594 // invalidated. This case should be quite rare 1595 cache.invalidate("Update", uri, selection); 1596 //$FALL-THROUGH$ 1597 default: 1598 result = db.update(tableName, values, selection, selectionArgs); 1599 break outer; 1600 } 1601 case ACCOUNT_RESET_NEW_COUNT_ID: 1602 id = uri.getPathSegments().get(1); 1603 if (cache != null) { 1604 cache.lock(id); 1605 } 1606 ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 1607 if (values != null) { 1608 Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME); 1609 if (set != null) { 1610 newMessageCount = new ContentValues(); 1611 newMessageCount.put(Account.NEW_MESSAGE_COUNT, set); 1612 } 1613 } 1614 try { 1615 result = db.update(tableName, newMessageCount, 1616 whereWithId(id, selection), selectionArgs); 1617 } finally { 1618 if (cache != null) { 1619 cache.unlock(id, values); 1620 } 1621 } 1622 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1623 break; 1624 case ACCOUNT_RESET_NEW_COUNT: 1625 result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT, 1626 selection, selectionArgs); 1627 // Affects all accounts. Just invalidate all account cache. 1628 cache.invalidate("Reset all new counts", null, null); 1629 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1630 break; 1631 default: 1632 throw new IllegalArgumentException("Unknown URI " + uri); 1633 } 1634 } catch (SQLiteException e) { 1635 checkDatabases(); 1636 throw e; 1637 } 1638 1639 // Notify all notifier cursors 1640 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id); 1641 1642 resolver.notifyChange(notificationUri, null); 1643 return result; 1644 } 1645 1646 /** 1647 * Returns the base notification URI for the given content type. 1648 * 1649 * @param match The type of content that was modified. 1650 */ 1651 private Uri getBaseNotificationUri(int match) { 1652 Uri baseUri = null; 1653 switch (match) { 1654 case MESSAGE: 1655 case MESSAGE_ID: 1656 case SYNCED_MESSAGE_ID: 1657 baseUri = Message.NOTIFIER_URI; 1658 break; 1659 case ACCOUNT: 1660 case ACCOUNT_ID: 1661 baseUri = Account.NOTIFIER_URI; 1662 break; 1663 } 1664 return baseUri; 1665 } 1666 1667 /** 1668 * Sends a change notification to any cursors observers of the given base URI. The final 1669 * notification URI is dynamically built to contain the specified information. It will be 1670 * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending 1671 * upon the given values. 1672 * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked. 1673 * If this is necessary, it can be added. However, due to the implementation of 1674 * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications. 1675 * 1676 * @param baseUri The base URI to send notifications to. Must be able to take appended IDs. 1677 * @param op Optional operation to be appended to the URI. 1678 * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be 1679 * appended to the base URI. 1680 */ 1681 private void sendNotifierChange(Uri baseUri, String op, String id) { 1682 if (baseUri == null) return; 1683 1684 final ContentResolver resolver = getContext().getContentResolver(); 1685 1686 // Append the operation, if specified 1687 if (op != null) { 1688 baseUri = baseUri.buildUpon().appendEncodedPath(op).build(); 1689 } 1690 1691 long longId = 0L; 1692 try { 1693 longId = Long.valueOf(id); 1694 } catch (NumberFormatException ignore) {} 1695 if (longId > 0) { 1696 resolver.notifyChange(ContentUris.withAppendedId(baseUri, longId), null); 1697 } else { 1698 resolver.notifyChange(baseUri, null); 1699 } 1700 1701 // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI. 1702 if (baseUri.equals(Message.NOTIFIER_URI)) { 1703 sendMessageListDataChangedNotification(); 1704 } 1705 } 1706 1707 private void sendMessageListDataChangedNotification() { 1708 final Context context = getContext(); 1709 final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED); 1710 // Ideally this intent would contain information about which account changed, to limit the 1711 // updates to that particular account. Unfortunately, that information is not available in 1712 // sendNotifierChange(). 1713 context.sendBroadcast(intent); 1714 } 1715 1716 @Override 1717 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1718 throws OperationApplicationException { 1719 Context context = getContext(); 1720 SQLiteDatabase db = getDatabase(context); 1721 db.beginTransaction(); 1722 try { 1723 ContentProviderResult[] results = super.applyBatch(operations); 1724 db.setTransactionSuccessful(); 1725 return results; 1726 } finally { 1727 db.endTransaction(); 1728 } 1729 } 1730 1731 /** 1732 * For testing purposes, check whether a given row is cached 1733 * @param baseUri the base uri of the EmailContent 1734 * @param id the row id of the EmailContent 1735 * @return whether or not the row is currently cached 1736 */ 1737 @VisibleForTesting 1738 protected boolean isCached(Uri baseUri, long id) { 1739 int match = findMatch(baseUri, "isCached"); 1740 int table = match >> BASE_SHIFT; 1741 ContentCache cache = mContentCaches[table]; 1742 if (cache == null) return false; 1743 Cursor cc = cache.get(Long.toString(id)); 1744 return (cc != null); 1745 } 1746 1747 public static interface AttachmentService { 1748 /** 1749 * Notify the service that an attachment has changed. 1750 */ 1751 void attachmentChanged(Context context, long id, int flags); 1752 } 1753 1754 private final AttachmentService DEFAULT_ATTACHMENT_SERVICE = new AttachmentService() { 1755 @Override 1756 public void attachmentChanged(Context context, long id, int flags) { 1757 // The default implementation delegates to the real service. 1758 AttachmentDownloadService.attachmentChanged(context, id, flags); 1759 } 1760 }; 1761 private AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE; 1762 1763 /** 1764 * Injects a custom attachment service handler. If null is specified, will reset to the 1765 * default service. 1766 */ 1767 public void injectAttachmentService(AttachmentService as) { 1768 mAttachmentService = (as == null) ? DEFAULT_ATTACHMENT_SERVICE : as; 1769 } 1770 1771 /** 1772 * Support for UnifiedEmail below 1773 */ 1774 1775 private static final String NOT_A_DRAFT_STRING = 1776 Integer.toString(UIProvider.DraftType.NOT_A_DRAFT); 1777 1778 /** 1779 * Mapping of UIProvider columns to EmailProvider columns for the message list (called the 1780 * conversation list in UnifiedEmail) 1781 */ 1782 private static final ProjectionMap sMessageListMap = ProjectionMap.builder() 1783 .add(BaseColumns._ID, MessageColumns.ID) 1784 .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage")) 1785 .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage")) 1786 .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT) 1787 .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET) 1788 .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST) 1789 .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) 1790 .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) 1791 .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1") 1792 .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0") 1793 .add(UIProvider.ConversationColumns.SENDING_STATE, 1794 Integer.toString(ConversationSendingState.OTHER)) 1795 .add(UIProvider.ConversationColumns.PRIORITY, Integer.toString(ConversationPriority.LOW)) 1796 .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ) 1797 .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE) 1798 .build(); 1799 1800 /** 1801 * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in 1802 * UnifiedEmail 1803 */ 1804 private static final ProjectionMap sMessageViewMap = ProjectionMap.builder() 1805 .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID) 1806 .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID) 1807 .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME)) 1808 .add(UIProvider.MessageColumns.CONVERSATION_ID, 1809 uriWithFQId("uimessage", Message.TABLE_NAME)) 1810 .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT) 1811 .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET) 1812 .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST) 1813 .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST) 1814 .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST) 1815 .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST) 1816 .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST) 1817 .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, EmailContent.MessageColumns.TIMESTAMP) 1818 .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT) 1819 .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT) 1820 .add(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, "0") 1821 .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0") 1822 .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING) 1823 .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0") 1824 .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, EmailContent.MessageColumns.FLAG_ATTACHMENT) 1825 .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, 1826 uriWithFQId("uiattachments", Message.TABLE_NAME)) 1827 .add(UIProvider.MessageColumns.MESSAGE_FLAGS, "0") 1828 .add(UIProvider.MessageColumns.SAVE_MESSAGE_URI, 1829 uriWithFQId("uiupdatedraft", Message.TABLE_NAME)) 1830 .add(UIProvider.MessageColumns.SEND_MESSAGE_URI, 1831 uriWithFQId("uisenddraft", Message.TABLE_NAME)) 1832 .build(); 1833 1834 /** 1835 * Mapping of UIProvider columns to EmailProvider columns for the folder list in UnifiedEmail 1836 */ 1837 private static final ProjectionMap sFolderListMap = ProjectionMap.builder() 1838 .add(BaseColumns._ID, MessageColumns.ID) 1839 .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) 1840 .add(UIProvider.FolderColumns.NAME, "displayName") 1841 .add(UIProvider.FolderColumns.HAS_CHILDREN, 1842 MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN) 1843 .add(UIProvider.FolderColumns.CAPABILITIES, "0") 1844 .add(UIProvider.FolderColumns.SYNC_FREQUENCY, "0") 1845 .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") 1846 .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) 1847 .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uichildren")) 1848 .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT) 1849 .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.MESSAGE_COUNT) 1850 .build(); 1851 1852 /** 1853 * The "ORDER BY" clause for top level folders 1854 */ 1855 private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE 1856 + " WHEN " + Mailbox.TYPE_INBOX + " THEN 0" 1857 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN 1" 1858 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN 2" 1859 + " WHEN " + Mailbox.TYPE_SENT + " THEN 3" 1860 + " WHEN " + Mailbox.TYPE_TRASH + " THEN 4" 1861 + " WHEN " + Mailbox.TYPE_JUNK + " THEN 5" 1862 // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order. 1863 + " ELSE 10 END" 1864 + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 1865 1866 /** 1867 * Generate the SELECT clause using a specified mapping and the original UI projection 1868 * @param map the ProjectionMap to use for this projection 1869 * @param projection the projection as sent by UnifiedEmail 1870 * @return a StringBuilder containing the SELECT expression for a SQLite query 1871 */ 1872 private StringBuilder genSelect(ProjectionMap map, String[] projection) { 1873 StringBuilder sb = new StringBuilder("SELECT "); 1874 boolean first = true; 1875 for (String column: projection) { 1876 if (first) { 1877 first = false; 1878 } else { 1879 sb.append(','); 1880 } 1881 String val = map.get(column); 1882 // If we don't have the column, be permissive, returning "0 AS <column>", and warn 1883 if (val == null) { 1884 Log.w(TAG, "UIProvider column not found, returning 0: " + column); 1885 val = "0 AS " + column; 1886 } 1887 sb.append(val); 1888 } 1889 return sb; 1890 } 1891 1892 /** 1893 * Convenience method to create a Uri string given the "type" of query; we append the type 1894 * of the query and the id column name (_id) 1895 * 1896 * @param type the "type" of the query, as defined by our UriMatcher definitions 1897 * @return a Uri string 1898 */ 1899 private static String uriWithId(String type) { 1900 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || _id"; 1901 } 1902 1903 /** 1904 * Convenience method to create a Uri string given the "type" of query and the table name to 1905 * which it applies; we append the type of the query and the fully qualified (FQ) id column 1906 * (i.e. including the table name); we need this for join queries where _id would otherwise 1907 * be ambiguous 1908 * 1909 * @param type the "type" of the query, as defined by our UriMatcher definitions 1910 * @param tableName the name of the table whose _id is referred to 1911 * @return a Uri string 1912 */ 1913 private static String uriWithFQId(String type, String tableName) { 1914 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id"; 1915 } 1916 1917 /** 1918 * Generate the "view message" SQLite query, given a projection from UnifiedEmail 1919 * 1920 * @param uiProjection as passed from UnifiedEmail 1921 * @return the SQLite query to be executed on the EmailProvider database 1922 */ 1923 private String genQueryViewMessage(String[] uiProjection) { 1924 StringBuilder sb = genSelect(sMessageViewMap, uiProjection); 1925 sb.append(" FROM " + Message.TABLE_NAME + "," + Body.TABLE_NAME + " WHERE " + 1926 Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " AND " + 1927 Message.TABLE_NAME + "." + Message.RECORD_ID + "=?"); 1928 return sb.toString(); 1929 } 1930 1931 /** 1932 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 1933 * 1934 * @param uiProjection as passed from UnifiedEmail 1935 * @return the SQLite query to be executed on the EmailProvider database 1936 */ 1937 private String genQueryMailboxMessages(String[] uiProjection) { 1938 StringBuilder sb = genSelect(sMessageListMap, uiProjection); 1939 // Make constant 1940 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.MAILBOX_KEY + "=? ORDER BY " + 1941 MessageColumns.TIMESTAMP + " DESC"); 1942 return sb.toString(); 1943 } 1944 1945 /** 1946 * Generate the "folder list" SQLite query, given a projection from UnifiedEmail 1947 * 1948 * @param uiProjection as passed from UnifiedEmail 1949 * @return the SQLite query to be executed on the EmailProvider database 1950 */ 1951 private String genQueryAccountMailboxes(String[] uiProjection) { 1952 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 1953 // Make constant 1954 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + Mailbox.ACCOUNT_KEY + "=? ORDER BY "); 1955 sb.append(MAILBOX_ORDER_BY); 1956 return sb.toString(); 1957 } 1958 1959 /** 1960 * Given the email address of an account, return its account id (the _id row in the Account 1961 * table), or NO_ACCOUNT (-1) if not found 1962 * 1963 * @param email the email address of the account 1964 * @return the account id for this account, or NO_ACCOUNT if not found 1965 */ 1966 private long findAccountIdByName(String email) { 1967 Map<String, Cursor> accountCache = mCacheAccount.getSnapshot(); 1968 Collection<Cursor> accounts = accountCache.values(); 1969 for (Cursor accountCursor: accounts) { 1970 if (accountCursor.getString(Account.CONTENT_EMAIL_ADDRESS_COLUMN).equals(email)) { 1971 return accountCursor.getLong(Account.CONTENT_ID_COLUMN); 1972 } 1973 } 1974 return Account.NO_ACCOUNT; 1975 } 1976 1977 /** 1978 * Handle UnifiedEmail queries here (dispatched from query()) 1979 * 1980 * @param match the UriMatcher match for the original uri passed in from UnifiedEmail 1981 * @param uri the original uri passed in from UnifiedEmail 1982 * @param uiProjection the projection passed in from UnifiedEmail 1983 * @return the result Cursor 1984 */ 1985 private Cursor uiQuery(int match, Uri uri, String[] uiProjection) { 1986 Context context = getContext(); 1987 SQLiteDatabase db = getDatabase(context); 1988 // Should we ever return null, or throw an exception?? 1989 Cursor c = null; 1990 switch(match) { 1991 case UI_FOLDERS: 1992 // We are passed the email address (unique account identifier) in the uri; we 1993 // need to turn this into the _id of the Account row in the EmailProvider db 1994 String accountName = uri.getPathSegments().get(1); 1995 long acctId = findAccountIdByName(accountName); 1996 if (acctId == Account.NO_ACCOUNT) return null; 1997 c = db.rawQuery(genQueryAccountMailboxes(uiProjection), 1998 new String[] {Long.toString(acctId)}); 1999 break; 2000 case UI_MESSAGES: 2001 String id = uri.getPathSegments().get(1); 2002 c = db.rawQuery(genQueryMailboxMessages(uiProjection), new String[] {id}); 2003 break; 2004 case UI_MESSAGE: 2005 id = uri.getPathSegments().get(1); 2006 c = db.rawQuery(genQueryViewMessage(uiProjection), new String[] {id}); 2007 break; 2008 } 2009 if (c != null) { 2010 // Notify UIProvider on changes 2011 // Make this more specific to actual query later on... 2012 c.setNotificationUri(context.getContentResolver(), UIPROVIDER_MESSAGE_NOTIFIER); 2013 } 2014 return c; 2015 } 2016 2017 /** 2018 * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need 2019 * a few of the fields 2020 * @param uiAtt the UIProvider attachment to convert 2021 * @return the EmailProvider attachment 2022 */ 2023 private Attachment convertUiAttachmentToAttachment( 2024 com.android.mail.providers.Attachment uiAtt) { 2025 Attachment att = new Attachment(); 2026 att.mContentUri = uiAtt.contentUri; 2027 att.mFileName = uiAtt.name; 2028 att.mMimeType = uiAtt.mimeType; 2029 att.mSize = uiAtt.size; 2030 return att; 2031 } 2032 2033 /** 2034 * Create a mailbox given the account and mailboxType. 2035 */ 2036 private Mailbox createMailbox(long accountId, int mailboxType) { 2037 Context context = getContext(); 2038 int resId = -1; 2039 switch (mailboxType) { 2040 case Mailbox.TYPE_INBOX: 2041 resId = R.string.mailbox_name_server_inbox; 2042 break; 2043 case Mailbox.TYPE_OUTBOX: 2044 resId = R.string.mailbox_name_server_outbox; 2045 break; 2046 case Mailbox.TYPE_DRAFTS: 2047 resId = R.string.mailbox_name_server_drafts; 2048 break; 2049 case Mailbox.TYPE_TRASH: 2050 resId = R.string.mailbox_name_server_trash; 2051 break; 2052 case Mailbox.TYPE_SENT: 2053 resId = R.string.mailbox_name_server_sent; 2054 break; 2055 case Mailbox.TYPE_JUNK: 2056 resId = R.string.mailbox_name_server_junk; 2057 break; 2058 default: 2059 throw new IllegalArgumentException("Illegal mailbox type"); 2060 } 2061 Log.d(TAG, "Creating mailbox of type " + mailboxType + " for account " + accountId); 2062 Mailbox box = Mailbox.newSystemMailbox(accountId, mailboxType, context.getString(resId)); 2063 box.save(context); 2064 return box; 2065 } 2066 2067 /** 2068 * Given an account name and a mailbox type, return that mailbox, creating it if necessary 2069 * @param accountName the account name to use 2070 * @param mailboxType the type of mailbox we're trying to find 2071 * @return the mailbox of the given type for the account in the uri, or null if not found 2072 */ 2073 private Mailbox getMailboxByUriAndType(String accountName, int mailboxType) { 2074 long accountId = findAccountIdByName(accountName); 2075 if (accountId == Account.NO_ACCOUNT) return null; 2076 Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), accountId, mailboxType); 2077 if (mailbox == null) { 2078 mailbox = createMailbox(accountId, mailboxType); 2079 } 2080 return mailbox; 2081 } 2082 2083 private Message getMessageFromPathSegments(List<String> pathSegments) { 2084 Message msg = null; 2085 if (pathSegments.size() > 2) { 2086 msg = Message.restoreMessageWithId(getContext(), Long.parseLong(pathSegments.get(2))); 2087 } 2088 if (msg == null) { 2089 msg = new Message(); 2090 } 2091 return msg; 2092 } 2093 /** 2094 * Given a mailbox and the content values for a message, create/save the message in the mailbox 2095 * @param mailbox the mailbox to use 2096 * @param values the content values that represent message fields 2097 * @return the uri of the newly created message 2098 */ 2099 private Uri uiSaveMessage(Message msg, Mailbox mailbox, ContentValues values) { 2100 Context context = getContext(); 2101 // Fill in the message 2102 msg.mTo = values.getAsString(UIProvider.MessageColumns.TO); 2103 msg.mCc = values.getAsString(UIProvider.MessageColumns.CC); 2104 msg.mBcc = values.getAsString(UIProvider.MessageColumns.BCC); 2105 msg.mSubject = values.getAsString(UIProvider.MessageColumns.SUBJECT); 2106 msg.mText = values.getAsString(UIProvider.MessageColumns.BODY_TEXT); 2107 msg.mHtml = values.getAsString(UIProvider.MessageColumns.BODY_HTML); 2108 msg.mMailboxKey = mailbox.mId; 2109 msg.mAccountKey = mailbox.mAccountKey; 2110 msg.mDisplayName = msg.mTo; 2111 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 2112 // Get attachments from the ContentValues 2113 ArrayList<com.android.mail.providers.Attachment> uiAtts = 2114 com.android.mail.providers.Attachment.getAttachmentsFromJoinedAttachmentInfo( 2115 values.getAsString(UIProvider.MessageColumns.JOINED_ATTACHMENT_INFOS)); 2116 ArrayList<Attachment> atts = new ArrayList<Attachment>(); 2117 for (com.android.mail.providers.Attachment uiAtt: uiAtts) { 2118 // Convert to our attachments and add to the list; everything else should "just work" 2119 atts.add(convertUiAttachmentToAttachment(uiAtt)); 2120 } 2121 if (!atts.isEmpty()) { 2122 msg.mAttachments = atts; 2123 } 2124 // Save it or update it... 2125 if (!msg.isSaved()) { 2126 msg.save(context); 2127 } else { 2128 // This is tricky due to how messages/attachments are saved; rather than putz with 2129 // what's changed, we'll delete/re-add them 2130 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 2131 // Delete all existing attachments 2132 ops.add(ContentProviderOperation.newDelete( 2133 ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId)) 2134 .build()); 2135 // Delete the body 2136 ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI) 2137 .withSelection(Body.MESSAGE_KEY + "=?", new String[] {Long.toString(msg.mId)}) 2138 .build()); 2139 // Add the ops for the message, atts, and body 2140 msg.addSaveOps(ops); 2141 // Do it! 2142 try { 2143 applyBatch(ops); 2144 } catch (OperationApplicationException e) { 2145 } 2146 } 2147 return Uri.parse("content://" + EmailContent.AUTHORITY + "/uimessage/" + msg.mId); 2148 } 2149 2150 /** 2151 * Create and send the message via the account indicated in the uri 2152 * @param uri the incoming uri 2153 * @param values the content values that represent message fields 2154 * @return the uri of the created message 2155 */ 2156 private Uri uiSendMail(Uri uri, ContentValues values) { 2157 List<String> pathSegments = uri.getPathSegments(); 2158 Mailbox mailbox = getMailboxByUriAndType(pathSegments.get(1), Mailbox.TYPE_OUTBOX); 2159 if (mailbox == null) return null; 2160 Message msg = getMessageFromPathSegments(pathSegments); 2161 try { 2162 return uiSaveMessage(msg, mailbox, values); 2163 } finally { 2164 // Kick observers 2165 getContext().getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 2166 } 2167 } 2168 2169 /** 2170 * Create a message and save it to the drafts folder of the account indicated in the uri 2171 * @param uri the incoming uri 2172 * @param values the content values that represent message fields 2173 * @return the uri of the created message 2174 */ 2175 private Uri uiSaveDraft(Uri uri, ContentValues values) { 2176 List<String> pathSegments = uri.getPathSegments(); 2177 Mailbox mailbox = getMailboxByUriAndType(pathSegments.get(1), Mailbox.TYPE_DRAFTS); 2178 if (mailbox == null) return null; 2179 Message msg = getMessageFromPathSegments(pathSegments); 2180 return uiSaveMessage(msg, mailbox, values); 2181 } 2182 2183 private int uiUpdateDraft(Uri uri, ContentValues values) { 2184 Context context = getContext(); 2185 Message msg = Message.restoreMessageWithId(context, 2186 Long.parseLong(uri.getPathSegments().get(1))); 2187 if (msg == null) return 0; 2188 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 2189 if (mailbox == null) return 0; 2190 uiSaveMessage(msg, mailbox, values); 2191 return 1; 2192 } 2193 2194 private int uiSendDraft(Uri uri, ContentValues values) { 2195 Context context = getContext(); 2196 Message msg = Message.restoreMessageWithId(context, 2197 Long.parseLong(uri.getPathSegments().get(1))); 2198 if (msg == null) return 0; 2199 long mailboxId = Mailbox.findMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_OUTBOX); 2200 if (mailboxId == Mailbox.NO_MAILBOX) return 0; 2201 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 2202 if (mailbox == null) return 0; 2203 uiSaveMessage(msg, mailbox, values); 2204 // Kick observers 2205 context.getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 2206 return 1; 2207 } 2208 2209 private void putIntegerLongOrBoolean(ContentValues values, String columnName, Object value) { 2210 if (value instanceof Integer) { 2211 Integer intValue = (Integer)value; 2212 values.put(columnName, intValue); 2213 } else if (value instanceof Boolean) { 2214 Boolean boolValue = (Boolean)value; 2215 values.put(columnName, boolValue ? 1 : 0); 2216 } else if (value instanceof Long) { 2217 Long longValue = (Long)value; 2218 values.put(columnName, longValue); 2219 } 2220 } 2221 2222 private ContentValues convertUiMessageValues(ContentValues values) { 2223 ContentValues ourValues = new ContentValues(); 2224 for (String columnName: values.keySet()) { 2225 Object val = values.get(columnName); 2226 if (columnName.equals(UIProvider.ConversationColumns.STARRED)) { 2227 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val); 2228 } else if (columnName.equals(UIProvider.ConversationColumns.READ)) { 2229 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val); 2230 } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 2231 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val); 2232 } else { 2233 throw new IllegalArgumentException("Can't update " + columnName + " in message"); 2234 } 2235 } 2236 return ourValues; 2237 } 2238 2239 private Uri convertToEmailProviderUri(Uri uri, boolean asProvider) { 2240 String idString = uri.getLastPathSegment(); 2241 try { 2242 long id = Long.parseLong(idString); 2243 Uri ourUri = ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id); 2244 if (asProvider) { 2245 ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build(); 2246 } 2247 return ourUri; 2248 } catch (NumberFormatException e) { 2249 return null; 2250 } 2251 } 2252 2253 private int uiUpdateMessage(Uri uri, ContentValues values) { 2254 Uri ourUri = convertToEmailProviderUri(uri, true); 2255 if (ourUri == null) return 0; 2256 ContentValues ourValues = convertUiMessageValues(values); 2257 return update(ourUri, ourValues, null, null); 2258 } 2259 2260 private int uiDeleteMessage(Uri uri) { 2261 Context context = getContext(); 2262 long messageId = Long.parseLong(uri.getPathSegments().get(1)); 2263 Message msg = Message.restoreMessageWithId(context, messageId); 2264 if (msg == null) return 0; 2265 Mailbox mailbox = 2266 Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH); 2267 if (mailbox == null) return 0; 2268 // See if we're the latest sequence 2269 String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER); 2270 if (sequenceString != null) { 2271 int sequence = Integer.parseInt(sequenceString); 2272 if (sequence > mLastSequence) { 2273 // Reset sequence 2274 mLastSequenceOps.clear(); 2275 mLastSequence = sequence; 2276 } 2277 // Handle errors here... 2278 // Add a reversing operation to the list 2279 // TODO: Need something to indicate a change isn't ready (undoable) 2280 mLastSequenceOps.add( 2281 ContentProviderOperation.newUpdate(convertToEmailProviderUri(uri, false)) 2282 .withValue(Message.MAILBOX_KEY, msg.mMailboxKey) 2283 .build()); 2284 } 2285 ContentValues values = new ContentValues(); 2286 values.put(Message.MAILBOX_KEY, mailbox.mId); 2287 return uiUpdateMessage(uri, values); 2288 } 2289 2290 private Cursor uiUndo(Uri uri, String[] projection) { 2291 // First see if we have any operations saved 2292 // TODO: Make sure seq matches 2293 if (!mLastSequenceOps.isEmpty()) { 2294 try { 2295 // TODO Always use this projection? Or what's passed in? 2296 // Not sure if UI wants it, but I'm making a cursor of convo uri's 2297 MatrixCursor c = new MatrixCursor( 2298 new String[] {UIProvider.ConversationColumns.URI}, 2299 mLastSequenceOps.size()); 2300 for (ContentProviderOperation op: mLastSequenceOps) { 2301 c.addRow(new String[] {op.getUri().toString()}); 2302 } 2303 // Just apply the batch and we're done! 2304 applyBatch(mLastSequenceOps); 2305 // But clear the operations 2306 mLastSequenceOps.clear(); 2307 return c; 2308 } catch (OperationApplicationException e) { 2309 } 2310 } 2311 return new MatrixCursor(projection, 0); 2312 } 2313} 2314