EmailProvider.java revision 83dae570ed7decef4e0be46a0f88cfee142b993f
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 notifyUIProvider("Delete"); 685 } 686 } 687 switch (match) { 688 case UI_MESSAGE: 689 return uiDeleteMessage(uri); 690 // These are cases in which one or more Messages might get deleted, either by 691 // cascade or explicitly 692 case MAILBOX_ID: 693 case MAILBOX: 694 case ACCOUNT_ID: 695 case ACCOUNT: 696 case MESSAGE: 697 case SYNCED_MESSAGE_ID: 698 case MESSAGE_ID: 699 // Handle lost Body records here, since this cannot be done in a trigger 700 // The process is: 701 // 1) Begin a transaction, ensuring that both databases are affected atomically 702 // 2) Do the requested deletion, with cascading deletions handled in triggers 703 // 3) End the transaction, committing all changes atomically 704 // 705 // Bodies are auto-deleted here; Attachments are auto-deleted via trigger 706 messageDeletion = true; 707 db.beginTransaction(); 708 break; 709 } 710 switch (match) { 711 case BODY_ID: 712 case DELETED_MESSAGE_ID: 713 case SYNCED_MESSAGE_ID: 714 case MESSAGE_ID: 715 case UPDATED_MESSAGE_ID: 716 case ATTACHMENT_ID: 717 case MAILBOX_ID: 718 case ACCOUNT_ID: 719 case HOSTAUTH_ID: 720 case POLICY_ID: 721 case QUICK_RESPONSE_ID: 722 id = uri.getPathSegments().get(1); 723 if (match == SYNCED_MESSAGE_ID) { 724 // For synced messages, first copy the old message to the deleted table and 725 // delete it from the updated table (in case it was updated first) 726 // Note that this is all within a transaction, for atomicity 727 db.execSQL(DELETED_MESSAGE_INSERT + id); 728 db.execSQL(UPDATED_MESSAGE_DELETE + id); 729 } 730 if (cache != null) { 731 cache.lock(id); 732 } 733 try { 734 result = db.delete(tableName, whereWithId(id, selection), selectionArgs); 735 if (cache != null) { 736 switch(match) { 737 case ACCOUNT_ID: 738 // Account deletion will clear all of the caches, as HostAuth's, 739 // Mailboxes, and Messages will be deleted in the process 740 mCacheMailbox.invalidate("Delete", uri, selection); 741 mCacheHostAuth.invalidate("Delete", uri, selection); 742 mCachePolicy.invalidate("Delete", uri, selection); 743 //$FALL-THROUGH$ 744 case MAILBOX_ID: 745 // Mailbox deletion will clear the Message cache 746 mCacheMessage.invalidate("Delete", uri, selection); 747 //$FALL-THROUGH$ 748 case SYNCED_MESSAGE_ID: 749 case MESSAGE_ID: 750 case HOSTAUTH_ID: 751 case POLICY_ID: 752 cache.invalidate("Delete", uri, selection); 753 // Make sure all data is properly cached 754 if (match != MESSAGE_ID) { 755 preCacheData(); 756 } 757 break; 758 } 759 } 760 } finally { 761 if (cache != null) { 762 cache.unlock(id); 763 } 764 } 765 break; 766 case ATTACHMENTS_MESSAGE_ID: 767 // All attachments for the given message 768 id = uri.getPathSegments().get(2); 769 result = db.delete(tableName, 770 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs); 771 break; 772 773 case BODY: 774 case MESSAGE: 775 case DELETED_MESSAGE: 776 case UPDATED_MESSAGE: 777 case ATTACHMENT: 778 case MAILBOX: 779 case ACCOUNT: 780 case HOSTAUTH: 781 case POLICY: 782 switch(match) { 783 // See the comments above for deletion of ACCOUNT_ID, etc 784 case ACCOUNT: 785 mCacheMailbox.invalidate("Delete", uri, selection); 786 mCacheHostAuth.invalidate("Delete", uri, selection); 787 mCachePolicy.invalidate("Delete", uri, selection); 788 //$FALL-THROUGH$ 789 case MAILBOX: 790 mCacheMessage.invalidate("Delete", uri, selection); 791 //$FALL-THROUGH$ 792 case MESSAGE: 793 case HOSTAUTH: 794 case POLICY: 795 cache.invalidate("Delete", uri, selection); 796 break; 797 } 798 result = db.delete(tableName, selection, selectionArgs); 799 switch(match) { 800 case ACCOUNT: 801 case MAILBOX: 802 case HOSTAUTH: 803 case POLICY: 804 // Make sure all data is properly cached 805 preCacheData(); 806 break; 807 } 808 break; 809 810 default: 811 throw new IllegalArgumentException("Unknown URI " + uri); 812 } 813 if (messageDeletion) { 814 if (match == MESSAGE_ID) { 815 // Delete the Body record associated with the deleted message 816 db.execSQL(DELETE_BODY + id); 817 } else { 818 // Delete any orphaned Body records 819 db.execSQL(DELETE_ORPHAN_BODIES); 820 } 821 db.setTransactionSuccessful(); 822 } 823 } catch (SQLiteException e) { 824 checkDatabases(); 825 throw e; 826 } finally { 827 if (messageDeletion) { 828 db.endTransaction(); 829 } 830 } 831 832 // Notify all notifier cursors 833 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id); 834 835 // Notify all email content cursors 836 resolver.notifyChange(EmailContent.CONTENT_URI, null); 837 return result; 838 } 839 840 @Override 841 // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) 842 public String getType(Uri uri) { 843 int match = findMatch(uri, "getType"); 844 switch (match) { 845 case BODY_ID: 846 return "vnd.android.cursor.item/email-body"; 847 case BODY: 848 return "vnd.android.cursor.dir/email-body"; 849 case UPDATED_MESSAGE_ID: 850 case MESSAGE_ID: 851 // NOTE: According to the framework folks, we're supposed to invent mime types as 852 // a way of passing information to drag & drop recipients. 853 // If there's a mailboxId parameter in the url, we respond with a mime type that 854 // has -n appended, where n is the mailboxId of the message. The drag & drop code 855 // uses this information to know not to allow dragging the item to its own mailbox 856 String mimeType = EMAIL_MESSAGE_MIME_TYPE; 857 String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID); 858 if (mailboxId != null) { 859 mimeType += "-" + mailboxId; 860 } 861 return mimeType; 862 case UPDATED_MESSAGE: 863 case MESSAGE: 864 return "vnd.android.cursor.dir/email-message"; 865 case MAILBOX: 866 return "vnd.android.cursor.dir/email-mailbox"; 867 case MAILBOX_ID: 868 return "vnd.android.cursor.item/email-mailbox"; 869 case ACCOUNT: 870 return "vnd.android.cursor.dir/email-account"; 871 case ACCOUNT_ID: 872 return "vnd.android.cursor.item/email-account"; 873 case ATTACHMENTS_MESSAGE_ID: 874 case ATTACHMENT: 875 return "vnd.android.cursor.dir/email-attachment"; 876 case ATTACHMENT_ID: 877 return EMAIL_ATTACHMENT_MIME_TYPE; 878 case HOSTAUTH: 879 return "vnd.android.cursor.dir/email-hostauth"; 880 case HOSTAUTH_ID: 881 return "vnd.android.cursor.item/email-hostauth"; 882 default: 883 throw new IllegalArgumentException("Unknown URI " + uri); 884 } 885 } 886 887 private static final Uri UIPROVIDER_MESSAGE_NOTIFIER = 888 Uri.parse("content://" + UIProvider.AUTHORITY + "/uimessages"); 889 890 @Override 891 public Uri insert(Uri uri, ContentValues values) { 892 int match = findMatch(uri, "insert"); 893 Context context = getContext(); 894 ContentResolver resolver = context.getContentResolver(); 895 896 // See the comment at delete(), above 897 SQLiteDatabase db = getDatabase(context); 898 int table = match >> BASE_SHIFT; 899 String id = "0"; 900 long longId; 901 902 // We do NOT allow setting of unreadCount/messageCount via the provider 903 // These columns are maintained via triggers 904 if (match == MAILBOX_ID || match == MAILBOX) { 905 values.put(MailboxColumns.UNREAD_COUNT, 0); 906 values.put(MailboxColumns.MESSAGE_COUNT, 0); 907 } 908 909 Uri resultUri = null; 910 911 try { 912 switch (match) { 913 case UI_SAVEDRAFT: 914 return uiSaveDraft(uri, values); 915 case UI_SENDMAIL: 916 return uiSendMail(uri, values); 917 // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE 918 // or DELETED_MESSAGE; see the comment below for details 919 case UPDATED_MESSAGE: 920 case DELETED_MESSAGE: 921 case MESSAGE: 922 case BODY: 923 case ATTACHMENT: 924 case MAILBOX: 925 case ACCOUNT: 926 case HOSTAUTH: 927 case POLICY: 928 case QUICK_RESPONSE: 929 longId = db.insert(TABLE_NAMES[table], "foo", values); 930 resultUri = ContentUris.withAppendedId(uri, longId); 931 switch(match) { 932 case MESSAGE: 933 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 934 notifyUIProvider("Insert"); 935 } 936 break; 937 case MAILBOX: 938 if (values.containsKey(MailboxColumns.TYPE)) { 939 // Only cache special mailbox types 940 int type = values.getAsInteger(MailboxColumns.TYPE); 941 if (type != Mailbox.TYPE_INBOX && type != Mailbox.TYPE_OUTBOX && 942 type != Mailbox.TYPE_DRAFTS && type != Mailbox.TYPE_SENT && 943 type != Mailbox.TYPE_TRASH && type != Mailbox.TYPE_SEARCH) { 944 break; 945 } 946 } 947 //$FALL-THROUGH$ 948 case ACCOUNT: 949 case HOSTAUTH: 950 case POLICY: 951 // Cache new account, host auth, policy, and some mailbox rows 952 Cursor c = query(resultUri, CACHE_PROJECTIONS[table], null, null, null); 953 if (c != null) { 954 if (match == MAILBOX) { 955 addToMailboxTypeMap(c); 956 } else if (match == ACCOUNT) { 957 getOrCreateAccountMailboxTypeMap(longId); 958 } 959 c.close(); 960 } 961 break; 962 } 963 // Clients shouldn't normally be adding rows to these tables, as they are 964 // maintained by triggers. However, we need to be able to do this for unit 965 // testing, so we allow the insert and then throw the same exception that we 966 // would if this weren't allowed. 967 if (match == UPDATED_MESSAGE || match == DELETED_MESSAGE) { 968 throw new IllegalArgumentException("Unknown URL " + uri); 969 } 970 if (match == ATTACHMENT) { 971 int flags = 0; 972 if (values.containsKey(Attachment.FLAGS)) { 973 flags = values.getAsInteger(Attachment.FLAGS); 974 } 975 // Report all new attachments to the download service 976 mAttachmentService.attachmentChanged(getContext(), longId, flags); 977 } 978 break; 979 case MAILBOX_ID: 980 // This implies adding a message to a mailbox 981 // Hmm, a problem here is that we can't link the account as well, so it must be 982 // already in the values... 983 longId = Long.parseLong(uri.getPathSegments().get(1)); 984 values.put(MessageColumns.MAILBOX_KEY, longId); 985 return insert(Message.CONTENT_URI, values); // Recurse 986 case MESSAGE_ID: 987 // This implies adding an attachment to a message. 988 id = uri.getPathSegments().get(1); 989 longId = Long.parseLong(id); 990 values.put(AttachmentColumns.MESSAGE_KEY, longId); 991 return insert(Attachment.CONTENT_URI, values); // Recurse 992 case ACCOUNT_ID: 993 // This implies adding a mailbox to an account. 994 longId = Long.parseLong(uri.getPathSegments().get(1)); 995 values.put(MailboxColumns.ACCOUNT_KEY, longId); 996 return insert(Mailbox.CONTENT_URI, values); // Recurse 997 case ATTACHMENTS_MESSAGE_ID: 998 longId = db.insert(TABLE_NAMES[table], "foo", values); 999 resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId); 1000 break; 1001 default: 1002 throw new IllegalArgumentException("Unknown URL " + uri); 1003 } 1004 } catch (SQLiteException e) { 1005 checkDatabases(); 1006 throw e; 1007 } 1008 1009 // Notify all notifier cursors 1010 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id); 1011 1012 // Notify all existing cursors. 1013 resolver.notifyChange(EmailContent.CONTENT_URI, null); 1014 return resultUri; 1015 } 1016 1017 @Override 1018 public boolean onCreate() { 1019 checkDatabases(); 1020 return false; 1021 } 1022 1023 /** 1024 * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must 1025 * always be in sync (i.e. there are two database or NO databases). This code will delete 1026 * any "orphan" database, so that both will be created together. Note that an "orphan" database 1027 * will exist after either of the individual databases is deleted due to data corruption. 1028 */ 1029 public void checkDatabases() { 1030 // Uncache the databases 1031 if (mDatabase != null) { 1032 mDatabase = null; 1033 } 1034 if (mBodyDatabase != null) { 1035 mBodyDatabase = null; 1036 } 1037 // Look for orphans, and delete as necessary; these must always be in sync 1038 File databaseFile = getContext().getDatabasePath(DATABASE_NAME); 1039 File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME); 1040 1041 // TODO Make sure attachments are deleted 1042 if (databaseFile.exists() && !bodyFile.exists()) { 1043 Log.w(TAG, "Deleting orphaned EmailProvider database..."); 1044 databaseFile.delete(); 1045 } else if (bodyFile.exists() && !databaseFile.exists()) { 1046 Log.w(TAG, "Deleting orphaned EmailProviderBody database..."); 1047 bodyFile.delete(); 1048 } 1049 } 1050 @Override 1051 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1052 String sortOrder) { 1053 long time = 0L; 1054 if (Email.DEBUG) { 1055 time = System.nanoTime(); 1056 } 1057 Cursor c = null; 1058 int match; 1059 try { 1060 match = findMatch(uri, "query"); 1061 } catch (IllegalArgumentException e) { 1062 String uriString = uri.toString(); 1063 // If we were passed an illegal uri, see if it ends in /-1 1064 // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor 1065 if (uriString != null && uriString.endsWith("/-1")) { 1066 uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0"); 1067 match = findMatch(uri, "query"); 1068 switch (match) { 1069 case BODY_ID: 1070 case MESSAGE_ID: 1071 case DELETED_MESSAGE_ID: 1072 case UPDATED_MESSAGE_ID: 1073 case ATTACHMENT_ID: 1074 case MAILBOX_ID: 1075 case ACCOUNT_ID: 1076 case HOSTAUTH_ID: 1077 case POLICY_ID: 1078 return new MatrixCursor(projection, 0); 1079 } 1080 } 1081 throw e; 1082 } 1083 Context context = getContext(); 1084 // See the comment at delete(), above 1085 SQLiteDatabase db = getDatabase(context); 1086 int table = match >> BASE_SHIFT; 1087 String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT); 1088 String id; 1089 1090 // Find the cache for this query's table (if any) 1091 ContentCache cache = null; 1092 String tableName = TABLE_NAMES[table]; 1093 // We can only use the cache if there's no selection 1094 if (selection == null) { 1095 cache = mContentCaches[table]; 1096 } 1097 if (cache == null) { 1098 ContentCache.notCacheable(uri, selection); 1099 } 1100 1101 try { 1102 switch (match) { 1103 // First, dispatch queries from UnfiedEmail 1104 case UI_UNDO: 1105 return uiUndo(uri, projection); 1106 case UI_FOLDERS: 1107 case UI_MESSAGES: 1108 case UI_MESSAGE: 1109 // For now, we don't allow selection criteria within these queries 1110 if (selection != null || selectionArgs != null) { 1111 throw new IllegalArgumentException("UI queries can't have selection/args"); 1112 } 1113 c = uiQuery(match, uri, projection); 1114 return c; 1115 case ACCOUNT_DEFAULT_ID: 1116 // Start with a snapshot of the cache 1117 Map<String, Cursor> accountCache = mCacheAccount.getSnapshot(); 1118 long accountId = Account.NO_ACCOUNT; 1119 // Find the account with "isDefault" set, or the lowest account ID otherwise. 1120 // Note that the snapshot from the cached isn't guaranteed to be sorted in any 1121 // way. 1122 Collection<Cursor> accounts = accountCache.values(); 1123 for (Cursor accountCursor: accounts) { 1124 // For now, at least, we can have zero count cursors (e.g. if someone looks 1125 // up a non-existent id); we need to skip these 1126 if (accountCursor.moveToFirst()) { 1127 boolean isDefault = 1128 accountCursor.getInt(Account.CONTENT_IS_DEFAULT_COLUMN) == 1; 1129 long iterId = accountCursor.getLong(Account.CONTENT_ID_COLUMN); 1130 // We'll remember this one if it's the default or the first one we see 1131 if (isDefault) { 1132 accountId = iterId; 1133 break; 1134 } else if ((accountId == Account.NO_ACCOUNT) || (iterId < accountId)) { 1135 accountId = iterId; 1136 } 1137 } 1138 } 1139 // Return a cursor with an id projection 1140 MatrixCursor mc = new MatrixCursor(EmailContent.ID_PROJECTION); 1141 mc.addRow(new Object[] {accountId}); 1142 c = mc; 1143 break; 1144 case MAILBOX_ID_FROM_ACCOUNT_AND_TYPE: 1145 // Get accountId and type and find the mailbox in our map 1146 List<String> pathSegments = uri.getPathSegments(); 1147 accountId = Long.parseLong(pathSegments.get(1)); 1148 int type = Integer.parseInt(pathSegments.get(2)); 1149 long mailboxId = getMailboxIdFromMailboxTypeMap(accountId, type); 1150 // Return a cursor with an id projection 1151 mc = new MatrixCursor(EmailContent.ID_PROJECTION); 1152 mc.addRow(new Object[] {mailboxId}); 1153 c = mc; 1154 break; 1155 case BODY: 1156 case MESSAGE: 1157 case UPDATED_MESSAGE: 1158 case DELETED_MESSAGE: 1159 case ATTACHMENT: 1160 case MAILBOX: 1161 case ACCOUNT: 1162 case HOSTAUTH: 1163 case POLICY: 1164 case QUICK_RESPONSE: 1165 // Special-case "count of accounts"; it's common and we always know it 1166 if (match == ACCOUNT && Arrays.equals(projection, EmailContent.COUNT_COLUMNS) && 1167 selection == null && limit.equals("1")) { 1168 int accountCount = mMailboxTypeMap.size(); 1169 // In the rare case there are MAX_CACHED_ACCOUNTS or more, we can't do this 1170 if (accountCount < MAX_CACHED_ACCOUNTS) { 1171 mc = new MatrixCursor(projection, 1); 1172 mc.addRow(new Object[] {accountCount}); 1173 c = mc; 1174 break; 1175 } 1176 } 1177 c = db.query(tableName, projection, 1178 selection, selectionArgs, null, null, sortOrder, limit); 1179 break; 1180 case BODY_ID: 1181 case MESSAGE_ID: 1182 case DELETED_MESSAGE_ID: 1183 case UPDATED_MESSAGE_ID: 1184 case ATTACHMENT_ID: 1185 case MAILBOX_ID: 1186 case ACCOUNT_ID: 1187 case HOSTAUTH_ID: 1188 case POLICY_ID: 1189 case QUICK_RESPONSE_ID: 1190 id = uri.getPathSegments().get(1); 1191 if (cache != null) { 1192 c = cache.getCachedCursor(id, projection); 1193 } 1194 if (c == null) { 1195 CacheToken token = null; 1196 if (cache != null) { 1197 token = cache.getCacheToken(id); 1198 } 1199 c = db.query(tableName, projection, whereWithId(id, selection), 1200 selectionArgs, null, null, sortOrder, limit); 1201 if (cache != null) { 1202 c = cache.putCursor(c, id, projection, token); 1203 } 1204 } 1205 break; 1206 case ATTACHMENTS_MESSAGE_ID: 1207 // All attachments for the given message 1208 id = uri.getPathSegments().get(2); 1209 c = db.query(Attachment.TABLE_NAME, projection, 1210 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), 1211 selectionArgs, null, null, sortOrder, limit); 1212 break; 1213 case QUICK_RESPONSE_ACCOUNT_ID: 1214 // All quick responses for the given account 1215 id = uri.getPathSegments().get(2); 1216 c = db.query(QuickResponse.TABLE_NAME, projection, 1217 whereWith(QuickResponse.ACCOUNT_KEY + "=" + id, selection), 1218 selectionArgs, null, null, sortOrder); 1219 break; 1220 default: 1221 throw new IllegalArgumentException("Unknown URI " + uri); 1222 } 1223 } catch (SQLiteException e) { 1224 checkDatabases(); 1225 throw e; 1226 } catch (RuntimeException e) { 1227 checkDatabases(); 1228 e.printStackTrace(); 1229 throw e; 1230 } finally { 1231 if (cache != null && c != null && Email.DEBUG) { 1232 cache.recordQueryTime(c, System.nanoTime() - time); 1233 } 1234 if (c == null) { 1235 // This should never happen, but let's be sure to log it... 1236 Log.e(TAG, "Query returning null for uri: " + uri + ", selection: " + selection); 1237 } 1238 } 1239 1240 if ((c != null) && !isTemporary()) { 1241 c.setNotificationUri(getContext().getContentResolver(), uri); 1242 } 1243 return c; 1244 } 1245 1246 private String whereWithId(String id, String selection) { 1247 StringBuilder sb = new StringBuilder(256); 1248 sb.append("_id="); 1249 sb.append(id); 1250 if (selection != null) { 1251 sb.append(" AND ("); 1252 sb.append(selection); 1253 sb.append(')'); 1254 } 1255 return sb.toString(); 1256 } 1257 1258 /** 1259 * Combine a locally-generated selection with a user-provided selection 1260 * 1261 * This introduces risk that the local selection might insert incorrect chars 1262 * into the SQL, so use caution. 1263 * 1264 * @param where locally-generated selection, must not be null 1265 * @param selection user-provided selection, may be null 1266 * @return a single selection string 1267 */ 1268 private String whereWith(String where, String selection) { 1269 if (selection == null) { 1270 return where; 1271 } 1272 StringBuilder sb = new StringBuilder(where); 1273 sb.append(" AND ("); 1274 sb.append(selection); 1275 sb.append(')'); 1276 1277 return sb.toString(); 1278 } 1279 1280 /** 1281 * Restore a HostAuth from a database, given its unique id 1282 * @param db the database 1283 * @param id the unique id (_id) of the row 1284 * @return a fully populated HostAuth or null if the row does not exist 1285 */ 1286 private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) { 1287 Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION, 1288 HostAuth.RECORD_ID + "=?", new String[] {Long.toString(id)}, null, null, null); 1289 try { 1290 if (c.moveToFirst()) { 1291 HostAuth hostAuth = new HostAuth(); 1292 hostAuth.restore(c); 1293 return hostAuth; 1294 } 1295 return null; 1296 } finally { 1297 c.close(); 1298 } 1299 } 1300 1301 /** 1302 * Copy the Account and HostAuth tables from one database to another 1303 * @param fromDatabase the source database 1304 * @param toDatabase the destination database 1305 * @return the number of accounts copied, or -1 if an error occurred 1306 */ 1307 private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) { 1308 if (fromDatabase == null || toDatabase == null) return -1; 1309 int copyCount = 0; 1310 try { 1311 // Lock both databases; for the "from" database, we don't want anyone changing it from 1312 // under us; for the "to" database, we want to make the operation atomic 1313 fromDatabase.beginTransaction(); 1314 toDatabase.beginTransaction(); 1315 // Delete anything hanging around here 1316 toDatabase.delete(Account.TABLE_NAME, null, null); 1317 toDatabase.delete(HostAuth.TABLE_NAME, null, null); 1318 // Get our account cursor 1319 Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION, 1320 null, null, null, null, null); 1321 boolean noErrors = true; 1322 try { 1323 // Loop through accounts, copying them and associated host auth's 1324 while (c.moveToNext()) { 1325 Account account = new Account(); 1326 account.restore(c); 1327 1328 // Clear security sync key and sync key, as these were specific to the state of 1329 // the account, and we've reset that... 1330 // Clear policy key so that we can re-establish policies from the server 1331 // TODO This is pretty EAS specific, but there's a lot of that around 1332 account.mSecuritySyncKey = null; 1333 account.mSyncKey = null; 1334 account.mPolicyKey = 0; 1335 1336 // Copy host auth's and update foreign keys 1337 HostAuth hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeyRecv); 1338 // The account might have gone away, though very unlikely 1339 if (hostAuth == null) continue; 1340 account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null, 1341 hostAuth.toContentValues()); 1342 // EAS accounts have no send HostAuth 1343 if (account.mHostAuthKeySend > 0) { 1344 hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend); 1345 // Belt and suspenders; I can't imagine that this is possible, since we 1346 // checked the validity of the account above, and the database is now locked 1347 if (hostAuth == null) continue; 1348 account.mHostAuthKeySend = toDatabase.insert(HostAuth.TABLE_NAME, null, 1349 hostAuth.toContentValues()); 1350 } 1351 // Now, create the account in the "to" database 1352 toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues()); 1353 copyCount++; 1354 } 1355 } catch (SQLiteException e) { 1356 noErrors = false; 1357 copyCount = -1; 1358 } finally { 1359 fromDatabase.endTransaction(); 1360 if (noErrors) { 1361 // Say it's ok to commit 1362 toDatabase.setTransactionSuccessful(); 1363 } 1364 toDatabase.endTransaction(); 1365 c.close(); 1366 } 1367 } catch (SQLiteException e) { 1368 copyCount = -1; 1369 } 1370 return copyCount; 1371 } 1372 1373 private static SQLiteDatabase getBackupDatabase(Context context) { 1374 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, BACKUP_DATABASE_NAME); 1375 return helper.getWritableDatabase(); 1376 } 1377 1378 /** 1379 * Backup account data, returning the number of accounts backed up 1380 */ 1381 private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) { 1382 if (Email.DEBUG) { 1383 Log.d(TAG, "backupAccounts..."); 1384 } 1385 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1386 try { 1387 int numBackedUp = copyAccountTables(mainDatabase, backupDatabase); 1388 if (numBackedUp < 0) { 1389 Log.e(TAG, "Account backup failed!"); 1390 } else if (Email.DEBUG) { 1391 Log.d(TAG, "Backed up " + numBackedUp + " accounts..."); 1392 } 1393 return numBackedUp; 1394 } finally { 1395 if (backupDatabase != null) { 1396 backupDatabase.close(); 1397 } 1398 } 1399 } 1400 1401 /** 1402 * Restore account data, returning the number of accounts restored 1403 */ 1404 private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) { 1405 if (Email.DEBUG) { 1406 Log.d(TAG, "restoreAccounts..."); 1407 } 1408 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1409 try { 1410 int numRecovered = copyAccountTables(backupDatabase, mainDatabase); 1411 if (numRecovered > 0) { 1412 Log.e(TAG, "Recovered " + numRecovered + " accounts!"); 1413 } else if (numRecovered < 0) { 1414 Log.e(TAG, "Account recovery failed?"); 1415 } else if (Email.DEBUG) { 1416 Log.d(TAG, "No accounts to restore..."); 1417 } 1418 return numRecovered; 1419 } finally { 1420 if (backupDatabase != null) { 1421 backupDatabase.close(); 1422 } 1423 } 1424 } 1425 1426 @Override 1427 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1428 // Handle this special case the fastest possible way 1429 if (uri == INTEGRITY_CHECK_URI) { 1430 checkDatabases(); 1431 return 0; 1432 } else if (uri == ACCOUNT_BACKUP_URI) { 1433 return backupAccounts(getContext(), getDatabase(getContext())); 1434 } 1435 1436 // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID) 1437 Uri notificationUri = EmailContent.CONTENT_URI; 1438 1439 int match = findMatch(uri, "update"); 1440 Context context = getContext(); 1441 ContentResolver resolver = context.getContentResolver(); 1442 // See the comment at delete(), above 1443 SQLiteDatabase db = getDatabase(context); 1444 int table = match >> BASE_SHIFT; 1445 int result; 1446 1447 // We do NOT allow setting of unreadCount/messageCount via the provider 1448 // These columns are maintained via triggers 1449 if (match == MAILBOX_ID || match == MAILBOX) { 1450 values.remove(MailboxColumns.UNREAD_COUNT); 1451 values.remove(MailboxColumns.MESSAGE_COUNT); 1452 } 1453 1454 ContentCache cache = mContentCaches[table]; 1455 String tableName = TABLE_NAMES[table]; 1456 String id = "0"; 1457 1458 try { 1459 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 1460 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 1461 notifyUIProvider("Update"); 1462 } 1463 } 1464outer: 1465 switch (match) { 1466 case UI_UPDATEDRAFT: 1467 return uiUpdateDraft(uri, values); 1468 case UI_SENDDRAFT: 1469 return uiSendDraft(uri, values); 1470 case UI_MESSAGE: 1471 return uiUpdateMessage(uri, values); 1472 case MAILBOX_ID_ADD_TO_FIELD: 1473 case ACCOUNT_ID_ADD_TO_FIELD: 1474 id = uri.getPathSegments().get(1); 1475 String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME); 1476 Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME); 1477 if (field == null || add == null) { 1478 throw new IllegalArgumentException("No field/add specified " + uri); 1479 } 1480 ContentValues actualValues = new ContentValues(); 1481 if (cache != null) { 1482 cache.lock(id); 1483 } 1484 try { 1485 db.beginTransaction(); 1486 try { 1487 Cursor c = db.query(tableName, 1488 new String[] {EmailContent.RECORD_ID, field}, 1489 whereWithId(id, selection), 1490 selectionArgs, null, null, null); 1491 try { 1492 result = 0; 1493 String[] bind = new String[1]; 1494 if (c.moveToNext()) { 1495 bind[0] = c.getString(0); // _id 1496 long value = c.getLong(1) + add; 1497 actualValues.put(field, value); 1498 result = db.update(tableName, actualValues, ID_EQUALS, bind); 1499 } 1500 db.setTransactionSuccessful(); 1501 } finally { 1502 c.close(); 1503 } 1504 } finally { 1505 db.endTransaction(); 1506 } 1507 } finally { 1508 if (cache != null) { 1509 cache.unlock(id, actualValues); 1510 } 1511 } 1512 break; 1513 case SYNCED_MESSAGE_ID: 1514 case UPDATED_MESSAGE_ID: 1515 case MESSAGE_ID: 1516 case BODY_ID: 1517 case ATTACHMENT_ID: 1518 case MAILBOX_ID: 1519 case ACCOUNT_ID: 1520 case HOSTAUTH_ID: 1521 case QUICK_RESPONSE_ID: 1522 case POLICY_ID: 1523 id = uri.getPathSegments().get(1); 1524 if (cache != null) { 1525 cache.lock(id); 1526 } 1527 try { 1528 if (match == SYNCED_MESSAGE_ID) { 1529 // For synced messages, first copy the old message to the updated table 1530 // Note the insert or ignore semantics, guaranteeing that only the first 1531 // update will be reflected in the updated message table; therefore this 1532 // row will always have the "original" data 1533 db.execSQL(UPDATED_MESSAGE_INSERT + id); 1534 } else if (match == MESSAGE_ID) { 1535 db.execSQL(UPDATED_MESSAGE_DELETE + id); 1536 } 1537 result = db.update(tableName, values, whereWithId(id, selection), 1538 selectionArgs); 1539 } catch (SQLiteException e) { 1540 // Null out values (so they aren't cached) and re-throw 1541 values = null; 1542 throw e; 1543 } finally { 1544 if (cache != null) { 1545 cache.unlock(id, values); 1546 } 1547 } 1548 if (match == ATTACHMENT_ID) { 1549 if (values.containsKey(Attachment.FLAGS)) { 1550 int flags = values.getAsInteger(Attachment.FLAGS); 1551 mAttachmentService.attachmentChanged(getContext(), 1552 Integer.parseInt(id), flags); 1553 } 1554 } 1555 break; 1556 case BODY: 1557 case MESSAGE: 1558 case UPDATED_MESSAGE: 1559 case ATTACHMENT: 1560 case MAILBOX: 1561 case ACCOUNT: 1562 case HOSTAUTH: 1563 case POLICY: 1564 switch(match) { 1565 // To avoid invalidating the cache on updates, we execute them one at a 1566 // time using the XXX_ID uri; these are all executed atomically 1567 case ACCOUNT: 1568 case MAILBOX: 1569 case HOSTAUTH: 1570 case POLICY: 1571 Cursor c = db.query(tableName, EmailContent.ID_PROJECTION, 1572 selection, selectionArgs, null, null, null); 1573 db.beginTransaction(); 1574 result = 0; 1575 try { 1576 while (c.moveToNext()) { 1577 update(ContentUris.withAppendedId( 1578 uri, c.getLong(EmailContent.ID_PROJECTION_COLUMN)), 1579 values, null, null); 1580 result++; 1581 } 1582 db.setTransactionSuccessful(); 1583 } finally { 1584 db.endTransaction(); 1585 c.close(); 1586 } 1587 break outer; 1588 // Any cached table other than those above should be invalidated here 1589 case MESSAGE: 1590 // If we're doing some generic update, the whole cache needs to be 1591 // invalidated. This case should be quite rare 1592 cache.invalidate("Update", uri, selection); 1593 //$FALL-THROUGH$ 1594 default: 1595 result = db.update(tableName, values, selection, selectionArgs); 1596 break outer; 1597 } 1598 case ACCOUNT_RESET_NEW_COUNT_ID: 1599 id = uri.getPathSegments().get(1); 1600 if (cache != null) { 1601 cache.lock(id); 1602 } 1603 ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 1604 if (values != null) { 1605 Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME); 1606 if (set != null) { 1607 newMessageCount = new ContentValues(); 1608 newMessageCount.put(Account.NEW_MESSAGE_COUNT, set); 1609 } 1610 } 1611 try { 1612 result = db.update(tableName, newMessageCount, 1613 whereWithId(id, selection), selectionArgs); 1614 } finally { 1615 if (cache != null) { 1616 cache.unlock(id, values); 1617 } 1618 } 1619 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1620 break; 1621 case ACCOUNT_RESET_NEW_COUNT: 1622 result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT, 1623 selection, selectionArgs); 1624 // Affects all accounts. Just invalidate all account cache. 1625 cache.invalidate("Reset all new counts", null, null); 1626 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1627 break; 1628 default: 1629 throw new IllegalArgumentException("Unknown URI " + uri); 1630 } 1631 } catch (SQLiteException e) { 1632 checkDatabases(); 1633 throw e; 1634 } 1635 1636 // Notify all notifier cursors 1637 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id); 1638 1639 resolver.notifyChange(notificationUri, null); 1640 return result; 1641 } 1642 1643 /** 1644 * Returns the base notification URI for the given content type. 1645 * 1646 * @param match The type of content that was modified. 1647 */ 1648 private Uri getBaseNotificationUri(int match) { 1649 Uri baseUri = null; 1650 switch (match) { 1651 case MESSAGE: 1652 case MESSAGE_ID: 1653 case SYNCED_MESSAGE_ID: 1654 baseUri = Message.NOTIFIER_URI; 1655 break; 1656 case ACCOUNT: 1657 case ACCOUNT_ID: 1658 baseUri = Account.NOTIFIER_URI; 1659 break; 1660 } 1661 return baseUri; 1662 } 1663 1664 /** 1665 * Sends a change notification to any cursors observers of the given base URI. The final 1666 * notification URI is dynamically built to contain the specified information. It will be 1667 * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending 1668 * upon the given values. 1669 * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked. 1670 * If this is necessary, it can be added. However, due to the implementation of 1671 * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications. 1672 * 1673 * @param baseUri The base URI to send notifications to. Must be able to take appended IDs. 1674 * @param op Optional operation to be appended to the URI. 1675 * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be 1676 * appended to the base URI. 1677 */ 1678 private void sendNotifierChange(Uri baseUri, String op, String id) { 1679 if (baseUri == null) return; 1680 1681 final ContentResolver resolver = getContext().getContentResolver(); 1682 1683 // Append the operation, if specified 1684 if (op != null) { 1685 baseUri = baseUri.buildUpon().appendEncodedPath(op).build(); 1686 } 1687 1688 long longId = 0L; 1689 try { 1690 longId = Long.valueOf(id); 1691 } catch (NumberFormatException ignore) {} 1692 if (longId > 0) { 1693 resolver.notifyChange(ContentUris.withAppendedId(baseUri, longId), null); 1694 } else { 1695 resolver.notifyChange(baseUri, null); 1696 } 1697 1698 // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI. 1699 if (baseUri.equals(Message.NOTIFIER_URI)) { 1700 sendMessageListDataChangedNotification(); 1701 } 1702 } 1703 1704 private void sendMessageListDataChangedNotification() { 1705 final Context context = getContext(); 1706 final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED); 1707 // Ideally this intent would contain information about which account changed, to limit the 1708 // updates to that particular account. Unfortunately, that information is not available in 1709 // sendNotifierChange(). 1710 context.sendBroadcast(intent); 1711 } 1712 1713 @Override 1714 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1715 throws OperationApplicationException { 1716 Context context = getContext(); 1717 SQLiteDatabase db = getDatabase(context); 1718 db.beginTransaction(); 1719 try { 1720 ContentProviderResult[] results = super.applyBatch(operations); 1721 db.setTransactionSuccessful(); 1722 return results; 1723 } finally { 1724 db.endTransaction(); 1725 } 1726 } 1727 1728 /** 1729 * For testing purposes, check whether a given row is cached 1730 * @param baseUri the base uri of the EmailContent 1731 * @param id the row id of the EmailContent 1732 * @return whether or not the row is currently cached 1733 */ 1734 @VisibleForTesting 1735 protected boolean isCached(Uri baseUri, long id) { 1736 int match = findMatch(baseUri, "isCached"); 1737 int table = match >> BASE_SHIFT; 1738 ContentCache cache = mContentCaches[table]; 1739 if (cache == null) return false; 1740 Cursor cc = cache.get(Long.toString(id)); 1741 return (cc != null); 1742 } 1743 1744 public static interface AttachmentService { 1745 /** 1746 * Notify the service that an attachment has changed. 1747 */ 1748 void attachmentChanged(Context context, long id, int flags); 1749 } 1750 1751 private final AttachmentService DEFAULT_ATTACHMENT_SERVICE = new AttachmentService() { 1752 @Override 1753 public void attachmentChanged(Context context, long id, int flags) { 1754 // The default implementation delegates to the real service. 1755 AttachmentDownloadService.attachmentChanged(context, id, flags); 1756 } 1757 }; 1758 private AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE; 1759 1760 /** 1761 * Injects a custom attachment service handler. If null is specified, will reset to the 1762 * default service. 1763 */ 1764 public void injectAttachmentService(AttachmentService as) { 1765 mAttachmentService = (as == null) ? DEFAULT_ATTACHMENT_SERVICE : as; 1766 } 1767 1768 /** 1769 * Support for UnifiedEmail below 1770 */ 1771 1772 private static final String NOT_A_DRAFT_STRING = 1773 Integer.toString(UIProvider.DraftType.NOT_A_DRAFT); 1774 1775 /** 1776 * Mapping of UIProvider columns to EmailProvider columns for the message list (called the 1777 * conversation list in UnifiedEmail) 1778 */ 1779 private static final ProjectionMap sMessageListMap = ProjectionMap.builder() 1780 .add(BaseColumns._ID, MessageColumns.ID) 1781 .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage")) 1782 .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage")) 1783 .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT) 1784 .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET) 1785 .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST) 1786 .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) 1787 .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) 1788 .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1") 1789 .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0") 1790 .add(UIProvider.ConversationColumns.SENDING_STATE, 1791 Integer.toString(ConversationSendingState.OTHER)) 1792 .add(UIProvider.ConversationColumns.PRIORITY, Integer.toString(ConversationPriority.LOW)) 1793 .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ) 1794 .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE) 1795 .build(); 1796 1797 /** 1798 * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in 1799 * UnifiedEmail 1800 */ 1801 private static final ProjectionMap sMessageViewMap = ProjectionMap.builder() 1802 .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID) 1803 .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID) 1804 .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME)) 1805 .add(UIProvider.MessageColumns.CONVERSATION_ID, 1806 uriWithFQId("uimessage", Message.TABLE_NAME)) 1807 .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT) 1808 .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET) 1809 .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST) 1810 .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST) 1811 .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST) 1812 .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST) 1813 .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST) 1814 .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, EmailContent.MessageColumns.TIMESTAMP) 1815 .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT) 1816 .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT) 1817 .add(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, "0") 1818 .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0") 1819 .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING) 1820 .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0") 1821 .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, EmailContent.MessageColumns.FLAG_ATTACHMENT) 1822 .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, 1823 uriWithFQId("uiattachments", Message.TABLE_NAME)) 1824 .add(UIProvider.MessageColumns.MESSAGE_FLAGS, "0") 1825 .add(UIProvider.MessageColumns.SAVE_MESSAGE_URI, 1826 uriWithFQId("uiupdatedraft", Message.TABLE_NAME)) 1827 .add(UIProvider.MessageColumns.SEND_MESSAGE_URI, 1828 uriWithFQId("uisenddraft", Message.TABLE_NAME)) 1829 .build(); 1830 1831 /** 1832 * Mapping of UIProvider columns to EmailProvider columns for the folder list in UnifiedEmail 1833 */ 1834 private static final ProjectionMap sFolderListMap = ProjectionMap.builder() 1835 .add(BaseColumns._ID, MessageColumns.ID) 1836 .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) 1837 .add(UIProvider.FolderColumns.NAME, "displayName") 1838 .add(UIProvider.FolderColumns.HAS_CHILDREN, 1839 MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN) 1840 .add(UIProvider.FolderColumns.CAPABILITIES, "0") 1841 .add(UIProvider.FolderColumns.SYNC_FREQUENCY, "0") 1842 .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") 1843 .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) 1844 .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uichildren")) 1845 .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT) 1846 .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.MESSAGE_COUNT) 1847 .build(); 1848 1849 /** 1850 * The "ORDER BY" clause for top level folders 1851 */ 1852 private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE 1853 + " WHEN " + Mailbox.TYPE_INBOX + " THEN 0" 1854 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN 1" 1855 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN 2" 1856 + " WHEN " + Mailbox.TYPE_SENT + " THEN 3" 1857 + " WHEN " + Mailbox.TYPE_TRASH + " THEN 4" 1858 + " WHEN " + Mailbox.TYPE_JUNK + " THEN 5" 1859 // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order. 1860 + " ELSE 10 END" 1861 + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 1862 1863 /** 1864 * Generate the SELECT clause using a specified mapping and the original UI projection 1865 * @param map the ProjectionMap to use for this projection 1866 * @param projection the projection as sent by UnifiedEmail 1867 * @return a StringBuilder containing the SELECT expression for a SQLite query 1868 */ 1869 private StringBuilder genSelect(ProjectionMap map, String[] projection) { 1870 StringBuilder sb = new StringBuilder("SELECT "); 1871 boolean first = true; 1872 for (String column: projection) { 1873 if (first) { 1874 first = false; 1875 } else { 1876 sb.append(','); 1877 } 1878 String val = map.get(column); 1879 // If we don't have the column, be permissive, returning "0 AS <column>", and warn 1880 if (val == null) { 1881 Log.w(TAG, "UIProvider column not found, returning 0: " + column); 1882 val = "0 AS " + column; 1883 } 1884 sb.append(val); 1885 } 1886 return sb; 1887 } 1888 1889 /** 1890 * Convenience method to create a Uri string given the "type" of query; we append the type 1891 * of the query and the id column name (_id) 1892 * 1893 * @param type the "type" of the query, as defined by our UriMatcher definitions 1894 * @return a Uri string 1895 */ 1896 private static String uriWithId(String type) { 1897 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || _id"; 1898 } 1899 1900 /** 1901 * Convenience method to create a Uri string given the "type" of query and the table name to 1902 * which it applies; we append the type of the query and the fully qualified (FQ) id column 1903 * (i.e. including the table name); we need this for join queries where _id would otherwise 1904 * be ambiguous 1905 * 1906 * @param type the "type" of the query, as defined by our UriMatcher definitions 1907 * @param tableName the name of the table whose _id is referred to 1908 * @return a Uri string 1909 */ 1910 private static String uriWithFQId(String type, String tableName) { 1911 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id"; 1912 } 1913 1914 /** 1915 * Generate the "view message" SQLite query, given a projection from UnifiedEmail 1916 * 1917 * @param uiProjection as passed from UnifiedEmail 1918 * @return the SQLite query to be executed on the EmailProvider database 1919 */ 1920 private String genQueryViewMessage(String[] uiProjection) { 1921 StringBuilder sb = genSelect(sMessageViewMap, uiProjection); 1922 sb.append(" FROM " + Message.TABLE_NAME + "," + Body.TABLE_NAME + " WHERE " + 1923 Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " AND " + 1924 Message.TABLE_NAME + "." + Message.RECORD_ID + "=?"); 1925 return sb.toString(); 1926 } 1927 1928 /** 1929 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 1930 * 1931 * @param uiProjection as passed from UnifiedEmail 1932 * @return the SQLite query to be executed on the EmailProvider database 1933 */ 1934 private String genQueryMailboxMessages(String[] uiProjection) { 1935 StringBuilder sb = genSelect(sMessageListMap, uiProjection); 1936 // Make constant 1937 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.MAILBOX_KEY + "=? ORDER BY " + 1938 MessageColumns.TIMESTAMP + " DESC"); 1939 return sb.toString(); 1940 } 1941 1942 /** 1943 * Generate the "folder list" SQLite query, given a projection from UnifiedEmail 1944 * 1945 * @param uiProjection as passed from UnifiedEmail 1946 * @return the SQLite query to be executed on the EmailProvider database 1947 */ 1948 private String genQueryAccountMailboxes(String[] uiProjection) { 1949 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 1950 // Make constant 1951 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + Mailbox.ACCOUNT_KEY + "=? ORDER BY "); 1952 sb.append(MAILBOX_ORDER_BY); 1953 return sb.toString(); 1954 } 1955 1956 /** 1957 * Given the email address of an account, return its account id (the _id row in the Account 1958 * table), or NO_ACCOUNT (-1) if not found 1959 * 1960 * @param email the email address of the account 1961 * @return the account id for this account, or NO_ACCOUNT if not found 1962 */ 1963 private long findAccountIdByName(String email) { 1964 Map<String, Cursor> accountCache = mCacheAccount.getSnapshot(); 1965 Collection<Cursor> accounts = accountCache.values(); 1966 for (Cursor accountCursor: accounts) { 1967 if (accountCursor.getString(Account.CONTENT_EMAIL_ADDRESS_COLUMN).equals(email)) { 1968 return accountCursor.getLong(Account.CONTENT_ID_COLUMN); 1969 } 1970 } 1971 return Account.NO_ACCOUNT; 1972 } 1973 1974 /** 1975 * Handle UnifiedEmail queries here (dispatched from query()) 1976 * 1977 * @param match the UriMatcher match for the original uri passed in from UnifiedEmail 1978 * @param uri the original uri passed in from UnifiedEmail 1979 * @param uiProjection the projection passed in from UnifiedEmail 1980 * @return the result Cursor 1981 */ 1982 private Cursor uiQuery(int match, Uri uri, String[] uiProjection) { 1983 Context context = getContext(); 1984 SQLiteDatabase db = getDatabase(context); 1985 // Should we ever return null, or throw an exception?? 1986 Cursor c = null; 1987 switch(match) { 1988 case UI_FOLDERS: 1989 // We are passed the email address (unique account identifier) in the uri; we 1990 // need to turn this into the _id of the Account row in the EmailProvider db 1991 String accountName = uri.getPathSegments().get(1); 1992 long acctId = findAccountIdByName(accountName); 1993 if (acctId == Account.NO_ACCOUNT) return null; 1994 c = db.rawQuery(genQueryAccountMailboxes(uiProjection), 1995 new String[] {Long.toString(acctId)}); 1996 break; 1997 case UI_MESSAGES: 1998 String id = uri.getPathSegments().get(1); 1999 c = db.rawQuery(genQueryMailboxMessages(uiProjection), new String[] {id}); 2000 break; 2001 case UI_MESSAGE: 2002 id = uri.getPathSegments().get(1); 2003 c = db.rawQuery(genQueryViewMessage(uiProjection), new String[] {id}); 2004 break; 2005 } 2006 if (c != null) { 2007 // Notify UIProvider on changes 2008 // Make this more specific to actual query later on... 2009 c.setNotificationUri(context.getContentResolver(), UIPROVIDER_MESSAGE_NOTIFIER); 2010 } 2011 return c; 2012 } 2013 2014 /** 2015 * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need 2016 * a few of the fields 2017 * @param uiAtt the UIProvider attachment to convert 2018 * @return the EmailProvider attachment 2019 */ 2020 private Attachment convertUiAttachmentToAttachment( 2021 com.android.mail.providers.Attachment uiAtt) { 2022 Attachment att = new Attachment(); 2023 att.mContentUri = uiAtt.contentUri; 2024 att.mFileName = uiAtt.name; 2025 att.mMimeType = uiAtt.mimeType; 2026 att.mSize = uiAtt.size; 2027 return att; 2028 } 2029 2030 /** 2031 * Create a mailbox given the account and mailboxType. 2032 */ 2033 private Mailbox createMailbox(long accountId, int mailboxType) { 2034 Context context = getContext(); 2035 int resId = -1; 2036 switch (mailboxType) { 2037 case Mailbox.TYPE_INBOX: 2038 resId = R.string.mailbox_name_server_inbox; 2039 break; 2040 case Mailbox.TYPE_OUTBOX: 2041 resId = R.string.mailbox_name_server_outbox; 2042 break; 2043 case Mailbox.TYPE_DRAFTS: 2044 resId = R.string.mailbox_name_server_drafts; 2045 break; 2046 case Mailbox.TYPE_TRASH: 2047 resId = R.string.mailbox_name_server_trash; 2048 break; 2049 case Mailbox.TYPE_SENT: 2050 resId = R.string.mailbox_name_server_sent; 2051 break; 2052 case Mailbox.TYPE_JUNK: 2053 resId = R.string.mailbox_name_server_junk; 2054 break; 2055 default: 2056 throw new IllegalArgumentException("Illegal mailbox type"); 2057 } 2058 Log.d(TAG, "Creating mailbox of type " + mailboxType + " for account " + accountId); 2059 Mailbox box = Mailbox.newSystemMailbox(accountId, mailboxType, context.getString(resId)); 2060 box.save(context); 2061 return box; 2062 } 2063 2064 /** 2065 * Given an account name and a mailbox type, return that mailbox, creating it if necessary 2066 * @param accountName the account name to use 2067 * @param mailboxType the type of mailbox we're trying to find 2068 * @return the mailbox of the given type for the account in the uri, or null if not found 2069 */ 2070 private Mailbox getMailboxByUriAndType(String accountName, int mailboxType) { 2071 long accountId = findAccountIdByName(accountName); 2072 if (accountId == Account.NO_ACCOUNT) return null; 2073 Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), accountId, mailboxType); 2074 if (mailbox == null) { 2075 mailbox = createMailbox(accountId, mailboxType); 2076 } 2077 return mailbox; 2078 } 2079 2080 private Message getMessageFromPathSegments(List<String> pathSegments) { 2081 Message msg = null; 2082 if (pathSegments.size() > 2) { 2083 msg = Message.restoreMessageWithId(getContext(), Long.parseLong(pathSegments.get(2))); 2084 } 2085 if (msg == null) { 2086 msg = new Message(); 2087 } 2088 return msg; 2089 } 2090 /** 2091 * Given a mailbox and the content values for a message, create/save the message in the mailbox 2092 * @param mailbox the mailbox to use 2093 * @param values the content values that represent message fields 2094 * @return the uri of the newly created message 2095 */ 2096 private Uri uiSaveMessage(Message msg, Mailbox mailbox, ContentValues values) { 2097 Context context = getContext(); 2098 // Fill in the message 2099 msg.mTo = values.getAsString(UIProvider.MessageColumns.TO); 2100 msg.mCc = values.getAsString(UIProvider.MessageColumns.CC); 2101 msg.mBcc = values.getAsString(UIProvider.MessageColumns.BCC); 2102 msg.mSubject = values.getAsString(UIProvider.MessageColumns.SUBJECT); 2103 msg.mText = values.getAsString(UIProvider.MessageColumns.BODY_TEXT); 2104 msg.mHtml = values.getAsString(UIProvider.MessageColumns.BODY_HTML); 2105 msg.mMailboxKey = mailbox.mId; 2106 msg.mAccountKey = mailbox.mAccountKey; 2107 msg.mDisplayName = msg.mTo; 2108 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 2109 // Get attachments from the ContentValues 2110 ArrayList<com.android.mail.providers.Attachment> uiAtts = 2111 com.android.mail.providers.Attachment.getAttachmentsFromJoinedAttachmentInfo( 2112 values.getAsString(UIProvider.MessageColumns.JOINED_ATTACHMENT_INFOS)); 2113 ArrayList<Attachment> atts = new ArrayList<Attachment>(); 2114 for (com.android.mail.providers.Attachment uiAtt: uiAtts) { 2115 // Convert to our attachments and add to the list; everything else should "just work" 2116 atts.add(convertUiAttachmentToAttachment(uiAtt)); 2117 } 2118 if (!atts.isEmpty()) { 2119 msg.mAttachments = atts; 2120 } 2121 // Save it or update it... 2122 if (!msg.isSaved()) { 2123 msg.save(context); 2124 } else { 2125 // This is tricky due to how messages/attachments are saved; rather than putz with 2126 // what's changed, we'll delete/re-add them 2127 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 2128 // Delete all existing attachments 2129 ops.add(ContentProviderOperation.newDelete( 2130 ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId)) 2131 .build()); 2132 // Delete the body 2133 ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI) 2134 .withSelection(Body.MESSAGE_KEY + "=?", new String[] {Long.toString(msg.mId)}) 2135 .build()); 2136 // Add the ops for the message, atts, and body 2137 msg.addSaveOps(ops); 2138 // Do it! 2139 try { 2140 applyBatch(ops); 2141 } catch (OperationApplicationException e) { 2142 } 2143 } 2144 return Uri.parse("content://" + EmailContent.AUTHORITY + "/uimessage/" + msg.mId); 2145 } 2146 2147 /** 2148 * Create and send the message via the account indicated in the uri 2149 * @param uri the incoming uri 2150 * @param values the content values that represent message fields 2151 * @return the uri of the created message 2152 */ 2153 private Uri uiSendMail(Uri uri, ContentValues values) { 2154 List<String> pathSegments = uri.getPathSegments(); 2155 Mailbox mailbox = getMailboxByUriAndType(pathSegments.get(1), Mailbox.TYPE_OUTBOX); 2156 if (mailbox == null) return null; 2157 Message msg = getMessageFromPathSegments(pathSegments); 2158 try { 2159 return uiSaveMessage(msg, mailbox, values); 2160 } finally { 2161 // Kick observers 2162 getContext().getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 2163 } 2164 } 2165 2166 /** 2167 * Create a message and save it to the drafts folder of the account indicated in the uri 2168 * @param uri the incoming uri 2169 * @param values the content values that represent message fields 2170 * @return the uri of the created message 2171 */ 2172 private Uri uiSaveDraft(Uri uri, ContentValues values) { 2173 List<String> pathSegments = uri.getPathSegments(); 2174 Mailbox mailbox = getMailboxByUriAndType(pathSegments.get(1), Mailbox.TYPE_DRAFTS); 2175 if (mailbox == null) return null; 2176 Message msg = getMessageFromPathSegments(pathSegments); 2177 return uiSaveMessage(msg, mailbox, values); 2178 } 2179 2180 private int uiUpdateDraft(Uri uri, ContentValues values) { 2181 Context context = getContext(); 2182 Message msg = Message.restoreMessageWithId(context, 2183 Long.parseLong(uri.getPathSegments().get(1))); 2184 if (msg == null) return 0; 2185 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 2186 if (mailbox == null) return 0; 2187 uiSaveMessage(msg, mailbox, values); 2188 return 1; 2189 } 2190 2191 private int uiSendDraft(Uri uri, ContentValues values) { 2192 Context context = getContext(); 2193 Message msg = Message.restoreMessageWithId(context, 2194 Long.parseLong(uri.getPathSegments().get(1))); 2195 if (msg == null) return 0; 2196 long mailboxId = Mailbox.findMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_OUTBOX); 2197 if (mailboxId == Mailbox.NO_MAILBOX) return 0; 2198 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 2199 if (mailbox == null) return 0; 2200 uiSaveMessage(msg, mailbox, values); 2201 // Kick observers 2202 context.getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 2203 return 1; 2204 } 2205 2206 private void putIntegerLongOrBoolean(ContentValues values, String columnName, Object value) { 2207 if (value instanceof Integer) { 2208 Integer intValue = (Integer)value; 2209 values.put(columnName, intValue); 2210 } else if (value instanceof Boolean) { 2211 Boolean boolValue = (Boolean)value; 2212 values.put(columnName, boolValue ? 1 : 0); 2213 } else if (value instanceof Long) { 2214 Long longValue = (Long)value; 2215 values.put(columnName, longValue); 2216 } 2217 } 2218 2219 private ContentValues convertUiMessageValues(ContentValues values) { 2220 ContentValues ourValues = new ContentValues(); 2221 for (String columnName: values.keySet()) { 2222 Object val = values.get(columnName); 2223 if (columnName.equals(UIProvider.ConversationColumns.STARRED)) { 2224 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val); 2225 } else if (columnName.equals(UIProvider.ConversationColumns.READ)) { 2226 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val); 2227 } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 2228 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val); 2229 } else if (columnName.equals(UIProvider.ConversationColumns.FOLDER_LIST)) { 2230 // Convert from folder list uri to mailbox key 2231 Uri uri = Uri.parse((String)val); 2232 Long mailboxId = Long.parseLong(uri.getLastPathSegment()); 2233 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId); 2234 } else { 2235 throw new IllegalArgumentException("Can't update " + columnName + " in message"); 2236 } 2237 } 2238 return ourValues; 2239 } 2240 2241 private Uri convertToEmailProviderUri(Uri uri, boolean asProvider) { 2242 String idString = uri.getLastPathSegment(); 2243 try { 2244 long id = Long.parseLong(idString); 2245 Uri ourUri = ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id); 2246 if (asProvider) { 2247 ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build(); 2248 } 2249 return ourUri; 2250 } catch (NumberFormatException e) { 2251 return null; 2252 } 2253 } 2254 2255 private Message getMessageFromLastSegment(Uri uri) { 2256 long messageId = Long.parseLong(uri.getLastPathSegment()); 2257 return Message.restoreMessageWithId(getContext(), messageId); 2258 } 2259 2260 /** 2261 * Add an undo operation for the current sequence; if the sequence is newer than what we've had, 2262 * clear out the undo list and start over 2263 * @param uri the uri we're working on 2264 * @param op the ContentProviderOperation to perform upon undo 2265 */ 2266 private void addToSequence(Uri uri, ContentProviderOperation op) { 2267 String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER); 2268 if (sequenceString != null) { 2269 int sequence = Integer.parseInt(sequenceString); 2270 if (sequence > mLastSequence) { 2271 // Reset sequence 2272 mLastSequenceOps.clear(); 2273 mLastSequence = sequence; 2274 } 2275 // TODO: Need something to indicate a change isn't ready (undoable) 2276 mLastSequenceOps.add(op); 2277 } 2278 } 2279 2280 private int uiUpdateMessage(Uri uri, ContentValues values) { 2281 Uri ourUri = convertToEmailProviderUri(uri, true); 2282 if (ourUri == null) return 0; 2283 ContentValues ourValues = convertUiMessageValues(values); 2284 Message msg = getMessageFromLastSegment(uri); 2285 if (msg == null) return 0; 2286 ContentValues undoValues = new ContentValues(); 2287 for (String columnName: ourValues.keySet()) { 2288 if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 2289 undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey); 2290 } else if (columnName.equals(MessageColumns.FLAG_READ)) { 2291 undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead); 2292 } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) { 2293 undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite); 2294 } 2295 } 2296 ContentProviderOperation op = 2297 ContentProviderOperation.newUpdate(convertToEmailProviderUri(uri, false)) 2298 .withValues(undoValues) 2299 .build(); 2300 addToSequence(uri, op); 2301 return update(ourUri, ourValues, null, null); 2302 } 2303 2304 private int uiDeleteMessage(Uri uri) { 2305 Context context = getContext(); 2306 Message msg = getMessageFromLastSegment(uri); 2307 if (msg == null) return 0; 2308 Mailbox mailbox = 2309 Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH); 2310 if (mailbox == null) return 0; 2311 ContentProviderOperation op = 2312 ContentProviderOperation.newUpdate(convertToEmailProviderUri(uri, false)) 2313 .withValue(Message.MAILBOX_KEY, msg.mMailboxKey) 2314 .build(); 2315 addToSequence(uri, op); 2316 ContentValues values = new ContentValues(); 2317 values.put(Message.MAILBOX_KEY, mailbox.mId); 2318 return uiUpdateMessage(uri, values); 2319 } 2320 2321 private Cursor uiUndo(Uri uri, String[] projection) { 2322 // First see if we have any operations saved 2323 // TODO: Make sure seq matches 2324 if (!mLastSequenceOps.isEmpty()) { 2325 try { 2326 // TODO Always use this projection? Or what's passed in? 2327 // Not sure if UI wants it, but I'm making a cursor of convo uri's 2328 MatrixCursor c = new MatrixCursor( 2329 new String[] {UIProvider.ConversationColumns.URI}, 2330 mLastSequenceOps.size()); 2331 for (ContentProviderOperation op: mLastSequenceOps) { 2332 c.addRow(new String[] {op.getUri().toString()}); 2333 } 2334 // Just apply the batch and we're done! 2335 applyBatch(mLastSequenceOps); 2336 // But clear the operations 2337 mLastSequenceOps.clear(); 2338 // Tell the UI there are changes 2339 notifyUIProvider("Undo"); 2340 return c; 2341 } catch (OperationApplicationException e) { 2342 } 2343 } 2344 return new MatrixCursor(projection, 0); 2345 } 2346 2347 private void notifyUIProvider(String reason) { 2348 getContext().getContentResolver().notifyChange(UIPROVIDER_MESSAGE_NOTIFIER, null); 2349 // Temporary 2350 Log.d(TAG, "[Notify UIProvider " + reason + "]"); 2351 } 2352} 2353