EmailProvider.java revision 1e138516eb980352af8ce70338bee4823b2adbc7
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_SUBFOLDERS = UI_BASE + 1; 187 private static final int UI_MESSAGES = UI_BASE + 2; 188 private static final int UI_MESSAGE = UI_BASE + 3; 189 private static final int UI_SENDMAIL = UI_BASE + 4; 190 private static final int UI_UNDO = UI_BASE + 5; 191 private static final int UI_SAVEDRAFT = UI_BASE + 6; 192 private static final int UI_UPDATEDRAFT = UI_BASE + 7; 193 private static final int UI_SENDDRAFT = UI_BASE + 8; 194 195 // MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS 196 private static final int LAST_EMAIL_PROVIDER_DB_BASE = UI_BASE; 197 198 // DO NOT CHANGE BODY_BASE!! 199 private static final int BODY_BASE = LAST_EMAIL_PROVIDER_DB_BASE + 0x1000; 200 private static final int BODY = BODY_BASE; 201 private static final int BODY_ID = BODY_BASE + 1; 202 203 private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc. 204 205 // TABLE_NAMES MUST remain in the order of the BASE constants above (e.g. ACCOUNT_BASE = 0x0000, 206 // MESSAGE_BASE = 0x1000, etc.) 207 private static final String[] TABLE_NAMES = { 208 Account.TABLE_NAME, 209 Mailbox.TABLE_NAME, 210 Message.TABLE_NAME, 211 Attachment.TABLE_NAME, 212 HostAuth.TABLE_NAME, 213 Message.UPDATED_TABLE_NAME, 214 Message.DELETED_TABLE_NAME, 215 Policy.TABLE_NAME, 216 QuickResponse.TABLE_NAME, 217 null, // UI 218 Body.TABLE_NAME, 219 }; 220 221 // CONTENT_CACHES MUST remain in the order of the BASE constants above 222 private final ContentCache[] mContentCaches = { 223 mCacheAccount, 224 mCacheMailbox, 225 mCacheMessage, 226 null, // Attachment 227 mCacheHostAuth, 228 null, // Updated message 229 null, // Deleted message 230 mCachePolicy, 231 null, // Quick response 232 null, // Body 233 null // UI 234 }; 235 236 // CACHE_PROJECTIONS MUST remain in the order of the BASE constants above 237 private static final String[][] CACHE_PROJECTIONS = { 238 Account.CONTENT_PROJECTION, 239 Mailbox.CONTENT_PROJECTION, 240 Message.CONTENT_PROJECTION, 241 null, // Attachment 242 HostAuth.CONTENT_PROJECTION, 243 null, // Updated message 244 null, // Deleted message 245 Policy.CONTENT_PROJECTION, 246 null, // Quick response 247 null, // Body 248 null // UI 249 }; 250 251 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 252 253 private static final String MAILBOX_PRE_CACHE_SELECTION = MailboxColumns.TYPE + " IN (" + 254 Mailbox.TYPE_INBOX + "," + Mailbox.TYPE_DRAFTS + "," + Mailbox.TYPE_TRASH + "," + 255 Mailbox.TYPE_SENT + "," + Mailbox.TYPE_SEARCH + "," + Mailbox.TYPE_OUTBOX + ")"; 256 257 /** 258 * Let's only generate these SQL strings once, as they are used frequently 259 * Note that this isn't relevant for table creation strings, since they are used only once 260 */ 261 private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " + 262 Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 263 EmailContent.RECORD_ID + '='; 264 265 private static final String UPDATED_MESSAGE_DELETE = "delete from " + 266 Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '='; 267 268 private static final String DELETED_MESSAGE_INSERT = "insert or replace into " + 269 Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 270 EmailContent.RECORD_ID + '='; 271 272 private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME + 273 " where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY + 274 " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " + 275 Message.TABLE_NAME + ')'; 276 277 private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME + 278 " where " + BodyColumns.MESSAGE_KEY + '='; 279 280 private static final String ID_EQUALS = EmailContent.RECORD_ID + "=?"; 281 282 private static final ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 283 284 public static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; 285 286 // For undo handling 287 private int mLastSequence = -1; 288 private ArrayList<ContentProviderOperation> mLastSequenceOps = 289 new ArrayList<ContentProviderOperation>(); 290 291 // Query parameter indicating the command came from UIProvider 292 private static final String IS_UIPROVIDER = "is_uiprovider"; 293 294 static { 295 // Email URI matching table 296 UriMatcher matcher = sURIMatcher; 297 298 // All accounts 299 matcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT); 300 // A specific account 301 // insert into this URI causes a mailbox to be added to the account 302 matcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID); 303 matcher.addURI(EmailContent.AUTHORITY, "account/default", ACCOUNT_DEFAULT_ID); 304 305 // Special URI to reset the new message count. Only update works, and content values 306 // will be ignored. 307 matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount", 308 ACCOUNT_RESET_NEW_COUNT); 309 matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount/#", 310 ACCOUNT_RESET_NEW_COUNT_ID); 311 312 // All mailboxes 313 matcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX); 314 // A specific mailbox 315 // insert into this URI causes a message to be added to the mailbox 316 // ** NOTE For now, the accountKey must be set manually in the values! 317 matcher.addURI(EmailContent.AUTHORITY, "mailbox/#", MAILBOX_ID); 318 matcher.addURI(EmailContent.AUTHORITY, "mailboxIdFromAccountAndType/#/#", 319 MAILBOX_ID_FROM_ACCOUNT_AND_TYPE); 320 // All messages 321 matcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE); 322 // A specific message 323 // insert into this URI causes an attachment to be added to the message 324 matcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID); 325 326 // A specific attachment 327 matcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT); 328 // A specific attachment (the header information) 329 matcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID); 330 // The attachments of a specific message (query only) (insert & delete TBD) 331 matcher.addURI(EmailContent.AUTHORITY, "attachment/message/#", 332 ATTACHMENTS_MESSAGE_ID); 333 334 // All mail bodies 335 matcher.addURI(EmailContent.AUTHORITY, "body", BODY); 336 // A specific mail body 337 matcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID); 338 339 // All hostauth records 340 matcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH); 341 // A specific hostauth 342 matcher.addURI(EmailContent.AUTHORITY, "hostauth/#", HOSTAUTH_ID); 343 344 // Atomically a constant value to a particular field of a mailbox/account 345 matcher.addURI(EmailContent.AUTHORITY, "mailboxIdAddToField/#", 346 MAILBOX_ID_ADD_TO_FIELD); 347 matcher.addURI(EmailContent.AUTHORITY, "accountIdAddToField/#", 348 ACCOUNT_ID_ADD_TO_FIELD); 349 350 /** 351 * THIS URI HAS SPECIAL SEMANTICS 352 * ITS USE IS INTENDED FOR THE UI APPLICATION TO MARK CHANGES THAT NEED TO BE SYNCED BACK 353 * TO A SERVER VIA A SYNC ADAPTER 354 */ 355 matcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); 356 357 /** 358 * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY 359 * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI 360 * BY THE UI APPLICATION 361 */ 362 // All deleted messages 363 matcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE); 364 // A specific deleted message 365 matcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID); 366 367 // All updated messages 368 matcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE); 369 // A specific updated message 370 matcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID); 371 372 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues(); 373 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0); 374 375 matcher.addURI(EmailContent.AUTHORITY, "policy", POLICY); 376 matcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID); 377 378 // All quick responses 379 matcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE); 380 // A specific quick response 381 matcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID); 382 // All quick responses associated with a particular account id 383 matcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#", 384 QUICK_RESPONSE_ACCOUNT_ID); 385 386 matcher.addURI(EmailContent.AUTHORITY, "uifolders/*", UI_FOLDERS); 387 matcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS); 388 matcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES); 389 matcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE); 390 matcher.addURI(EmailContent.AUTHORITY, "uisendmail/*", UI_SENDMAIL); 391 matcher.addURI(EmailContent.AUTHORITY, "uiundo/*", UI_UNDO); 392 matcher.addURI(EmailContent.AUTHORITY, "uisavedraft/*", UI_SAVEDRAFT); 393 matcher.addURI(EmailContent.AUTHORITY, "uiupdatedraft/#", UI_UPDATEDRAFT); 394 matcher.addURI(EmailContent.AUTHORITY, "uisenddraft/#", UI_SENDDRAFT); 395 } 396 397 /** 398 * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in 399 * @param uri the Uri to match 400 * @return the match value 401 */ 402 private static int findMatch(Uri uri, String methodName) { 403 int match = sURIMatcher.match(uri); 404 if (match < 0) { 405 throw new IllegalArgumentException("Unknown uri: " + uri); 406 } else if (Logging.LOGD) { 407 Log.v(TAG, methodName + ": uri=" + uri + ", match is " + match); 408 } 409 return match; 410 } 411 412 private SQLiteDatabase mDatabase; 413 private SQLiteDatabase mBodyDatabase; 414 415 /** 416 * Orphan record deletion utility. Generates a sqlite statement like: 417 * delete from <table> where <column> not in (select <foreignColumn> from <foreignTable>) 418 * @param db the EmailProvider database 419 * @param table the table whose orphans are to be removed 420 * @param column the column deletion will be based on 421 * @param foreignColumn the column in the foreign table whose absence will trigger the deletion 422 * @param foreignTable the foreign table 423 */ 424 @VisibleForTesting 425 void deleteUnlinked(SQLiteDatabase db, String table, String column, String foreignColumn, 426 String foreignTable) { 427 int count = db.delete(table, column + " not in (select " + foreignColumn + " from " + 428 foreignTable + ")", null); 429 if (count > 0) { 430 Log.w(TAG, "Found " + count + " orphaned row(s) in " + table); 431 } 432 } 433 434 @VisibleForTesting 435 synchronized SQLiteDatabase getDatabase(Context context) { 436 // Always return the cached database, if we've got one 437 if (mDatabase != null) { 438 return mDatabase; 439 } 440 441 // Whenever we create or re-cache the databases, make sure that we haven't lost one 442 // to corruption 443 checkDatabases(); 444 445 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); 446 mDatabase = helper.getWritableDatabase(); 447 DBHelper.BodyDatabaseHelper bodyHelper = 448 new DBHelper.BodyDatabaseHelper(context, BODY_DATABASE_NAME); 449 mBodyDatabase = bodyHelper.getWritableDatabase(); 450 if (mBodyDatabase != null) { 451 String bodyFileName = mBodyDatabase.getPath(); 452 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); 453 } 454 455 // Restore accounts if the database is corrupted... 456 restoreIfNeeded(context, mDatabase); 457 458 if (Email.DEBUG) { 459 Log.d(TAG, "Deleting orphans..."); 460 } 461 // Check for any orphaned Messages in the updated/deleted tables 462 deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME); 463 deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME); 464 // Delete orphaned mailboxes/messages/policies (account no longer exists) 465 deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY, AccountColumns.ID, 466 Account.TABLE_NAME); 467 deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY, AccountColumns.ID, 468 Account.TABLE_NAME); 469 deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns.ID, AccountColumns.POLICY_KEY, 470 Account.TABLE_NAME); 471 472 if (Email.DEBUG) { 473 Log.d(TAG, "EmailProvider pre-caching..."); 474 } 475 preCacheData(); 476 if (Email.DEBUG) { 477 Log.d(TAG, "EmailProvider ready."); 478 } 479 return mDatabase; 480 } 481 482 /** 483 * Pre-cache all of the items in a given table meeting the selection criteria 484 * @param tableUri the table uri 485 * @param baseProjection the base projection of that table 486 * @param selection the selection criteria 487 */ 488 private void preCacheTable(Uri tableUri, String[] baseProjection, String selection) { 489 Cursor c = query(tableUri, EmailContent.ID_PROJECTION, selection, null, null); 490 try { 491 while (c.moveToNext()) { 492 long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 493 Cursor cachedCursor = query(ContentUris.withAppendedId( 494 tableUri, id), baseProjection, null, null, null); 495 if (cachedCursor != null) { 496 // For accounts, create a mailbox type map entry (if necessary) 497 if (tableUri == Account.CONTENT_URI) { 498 getOrCreateAccountMailboxTypeMap(id); 499 } 500 cachedCursor.close(); 501 } 502 } 503 } finally { 504 c.close(); 505 } 506 } 507 508 private final HashMap<Long, HashMap<Integer, Long>> mMailboxTypeMap = 509 new HashMap<Long, HashMap<Integer, Long>>(); 510 511 private HashMap<Integer, Long> getOrCreateAccountMailboxTypeMap(long accountId) { 512 synchronized(mMailboxTypeMap) { 513 HashMap<Integer, Long> accountMailboxTypeMap = mMailboxTypeMap.get(accountId); 514 if (accountMailboxTypeMap == null) { 515 accountMailboxTypeMap = new HashMap<Integer, Long>(); 516 mMailboxTypeMap.put(accountId, accountMailboxTypeMap); 517 } 518 return accountMailboxTypeMap; 519 } 520 } 521 522 private void addToMailboxTypeMap(Cursor c) { 523 long accountId = c.getLong(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN); 524 int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 525 synchronized(mMailboxTypeMap) { 526 HashMap<Integer, Long> accountMailboxTypeMap = 527 getOrCreateAccountMailboxTypeMap(accountId); 528 accountMailboxTypeMap.put(type, c.getLong(Mailbox.CONTENT_ID_COLUMN)); 529 } 530 } 531 532 private long getMailboxIdFromMailboxTypeMap(long accountId, int type) { 533 synchronized(mMailboxTypeMap) { 534 HashMap<Integer, Long> accountMap = mMailboxTypeMap.get(accountId); 535 Long mailboxId = null; 536 if (accountMap != null) { 537 mailboxId = accountMap.get(type); 538 } 539 if (mailboxId == null) return Mailbox.NO_MAILBOX; 540 return mailboxId; 541 } 542 } 543 544 private void preCacheData() { 545 synchronized(mMailboxTypeMap) { 546 mMailboxTypeMap.clear(); 547 548 // Pre-cache accounts, host auth's, policies, and special mailboxes 549 preCacheTable(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null); 550 preCacheTable(HostAuth.CONTENT_URI, HostAuth.CONTENT_PROJECTION, null); 551 preCacheTable(Policy.CONTENT_URI, Policy.CONTENT_PROJECTION, null); 552 preCacheTable(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 553 MAILBOX_PRE_CACHE_SELECTION); 554 555 // Create a map from account,type to a mailbox 556 Map<String, Cursor> snapshot = mCacheMailbox.getSnapshot(); 557 Collection<Cursor> values = snapshot.values(); 558 if (values != null) { 559 for (Cursor c: values) { 560 if (c.moveToFirst()) { 561 addToMailboxTypeMap(c); 562 } 563 } 564 } 565 } 566 } 567 568 /*package*/ static SQLiteDatabase getReadableDatabase(Context context) { 569 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); 570 return helper.getReadableDatabase(); 571 } 572 573 /** 574 * Restore user Account and HostAuth data from our backup database 575 */ 576 public static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) { 577 if (Email.DEBUG) { 578 Log.w(TAG, "restoreIfNeeded..."); 579 } 580 // Check for legacy backup 581 String legacyBackup = Preferences.getLegacyBackupPreference(context); 582 // If there's a legacy backup, create a new-style backup and delete the legacy backup 583 // In the 1:1000000000 chance that the user gets an app update just as his database becomes 584 // corrupt, oh well... 585 if (!TextUtils.isEmpty(legacyBackup)) { 586 backupAccounts(context, mainDatabase); 587 Preferences.clearLegacyBackupPreference(context); 588 Log.w(TAG, "Created new EmailProvider backup database"); 589 return; 590 } 591 592 // If we have accounts, we're done 593 Cursor c = mainDatabase.query(Account.TABLE_NAME, EmailContent.ID_PROJECTION, null, null, 594 null, null, null); 595 if (c.moveToFirst()) { 596 if (Email.DEBUG) { 597 Log.w(TAG, "restoreIfNeeded: Account exists."); 598 } 599 return; // At least one account exists. 600 } 601 restoreAccounts(context, mainDatabase); 602 } 603 604 /** {@inheritDoc} */ 605 @Override 606 public void shutdown() { 607 if (mDatabase != null) { 608 mDatabase.close(); 609 mDatabase = null; 610 } 611 if (mBodyDatabase != null) { 612 mBodyDatabase.close(); 613 mBodyDatabase = null; 614 } 615 } 616 617 /*package*/ static void deleteMessageOrphans(SQLiteDatabase database, String tableName) { 618 if (database != null) { 619 // We'll look at all of the items in the table; there won't be many typically 620 Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); 621 // Usually, there will be nothing in these tables, so make a quick check 622 try { 623 if (c.getCount() == 0) return; 624 ArrayList<Long> foundMailboxes = new ArrayList<Long>(); 625 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>(); 626 ArrayList<Long> deleteList = new ArrayList<Long>(); 627 String[] bindArray = new String[1]; 628 while (c.moveToNext()) { 629 // Get the mailbox key and see if we've already found this mailbox 630 // If so, we're fine 631 long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY); 632 // If we already know this mailbox doesn't exist, mark the message for deletion 633 if (notFoundMailboxes.contains(mailboxId)) { 634 deleteList.add(c.getLong(ORPHANS_ID)); 635 // If we don't know about this mailbox, we'll try to find it 636 } else if (!foundMailboxes.contains(mailboxId)) { 637 bindArray[0] = Long.toString(mailboxId); 638 Cursor boxCursor = database.query(Mailbox.TABLE_NAME, 639 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null); 640 try { 641 // If it exists, we'll add it to the "found" mailboxes 642 if (boxCursor.moveToFirst()) { 643 foundMailboxes.add(mailboxId); 644 // Otherwise, we'll add to "not found" and mark the message for deletion 645 } else { 646 notFoundMailboxes.add(mailboxId); 647 deleteList.add(c.getLong(ORPHANS_ID)); 648 } 649 } finally { 650 boxCursor.close(); 651 } 652 } 653 } 654 // Now, delete the orphan messages 655 for (long messageId: deleteList) { 656 bindArray[0] = Long.toString(messageId); 657 database.delete(tableName, WHERE_ID, bindArray); 658 } 659 } finally { 660 c.close(); 661 } 662 } 663 } 664 665 @Override 666 public int delete(Uri uri, String selection, String[] selectionArgs) { 667 final int match = findMatch(uri, "delete"); 668 Context context = getContext(); 669 // Pick the correct database for this operation 670 // If we're in a transaction already (which would happen during applyBatch), then the 671 // body database is already attached to the email database and any attempt to use the 672 // body database directly will result in a SQLiteException (the database is locked) 673 SQLiteDatabase db = getDatabase(context); 674 int table = match >> BASE_SHIFT; 675 String id = "0"; 676 boolean messageDeletion = false; 677 ContentResolver resolver = context.getContentResolver(); 678 679 ContentCache cache = mContentCaches[table]; 680 String tableName = TABLE_NAMES[table]; 681 int result = -1; 682 683 try { 684 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 685 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 686 notifyUIProvider("Delete"); 687 } 688 } 689 switch (match) { 690 case UI_MESSAGE: 691 return uiDeleteMessage(uri); 692 // These are cases in which one or more Messages might get deleted, either by 693 // cascade or explicitly 694 case MAILBOX_ID: 695 case MAILBOX: 696 case ACCOUNT_ID: 697 case ACCOUNT: 698 case MESSAGE: 699 case SYNCED_MESSAGE_ID: 700 case MESSAGE_ID: 701 // Handle lost Body records here, since this cannot be done in a trigger 702 // The process is: 703 // 1) Begin a transaction, ensuring that both databases are affected atomically 704 // 2) Do the requested deletion, with cascading deletions handled in triggers 705 // 3) End the transaction, committing all changes atomically 706 // 707 // Bodies are auto-deleted here; Attachments are auto-deleted via trigger 708 messageDeletion = true; 709 db.beginTransaction(); 710 break; 711 } 712 switch (match) { 713 case BODY_ID: 714 case DELETED_MESSAGE_ID: 715 case SYNCED_MESSAGE_ID: 716 case MESSAGE_ID: 717 case UPDATED_MESSAGE_ID: 718 case ATTACHMENT_ID: 719 case MAILBOX_ID: 720 case ACCOUNT_ID: 721 case HOSTAUTH_ID: 722 case POLICY_ID: 723 case QUICK_RESPONSE_ID: 724 id = uri.getPathSegments().get(1); 725 if (match == SYNCED_MESSAGE_ID) { 726 // For synced messages, first copy the old message to the deleted table and 727 // delete it from the updated table (in case it was updated first) 728 // Note that this is all within a transaction, for atomicity 729 db.execSQL(DELETED_MESSAGE_INSERT + id); 730 db.execSQL(UPDATED_MESSAGE_DELETE + id); 731 } 732 if (cache != null) { 733 cache.lock(id); 734 } 735 try { 736 result = db.delete(tableName, whereWithId(id, selection), selectionArgs); 737 if (cache != null) { 738 switch(match) { 739 case ACCOUNT_ID: 740 // Account deletion will clear all of the caches, as HostAuth's, 741 // Mailboxes, and Messages will be deleted in the process 742 mCacheMailbox.invalidate("Delete", uri, selection); 743 mCacheHostAuth.invalidate("Delete", uri, selection); 744 mCachePolicy.invalidate("Delete", uri, selection); 745 //$FALL-THROUGH$ 746 case MAILBOX_ID: 747 // Mailbox deletion will clear the Message cache 748 mCacheMessage.invalidate("Delete", uri, selection); 749 //$FALL-THROUGH$ 750 case SYNCED_MESSAGE_ID: 751 case MESSAGE_ID: 752 case HOSTAUTH_ID: 753 case POLICY_ID: 754 cache.invalidate("Delete", uri, selection); 755 // Make sure all data is properly cached 756 if (match != MESSAGE_ID) { 757 preCacheData(); 758 } 759 break; 760 } 761 } 762 } finally { 763 if (cache != null) { 764 cache.unlock(id); 765 } 766 } 767 break; 768 case ATTACHMENTS_MESSAGE_ID: 769 // All attachments for the given message 770 id = uri.getPathSegments().get(2); 771 result = db.delete(tableName, 772 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs); 773 break; 774 775 case BODY: 776 case MESSAGE: 777 case DELETED_MESSAGE: 778 case UPDATED_MESSAGE: 779 case ATTACHMENT: 780 case MAILBOX: 781 case ACCOUNT: 782 case HOSTAUTH: 783 case POLICY: 784 switch(match) { 785 // See the comments above for deletion of ACCOUNT_ID, etc 786 case ACCOUNT: 787 mCacheMailbox.invalidate("Delete", uri, selection); 788 mCacheHostAuth.invalidate("Delete", uri, selection); 789 mCachePolicy.invalidate("Delete", uri, selection); 790 //$FALL-THROUGH$ 791 case MAILBOX: 792 mCacheMessage.invalidate("Delete", uri, selection); 793 //$FALL-THROUGH$ 794 case MESSAGE: 795 case HOSTAUTH: 796 case POLICY: 797 cache.invalidate("Delete", uri, selection); 798 break; 799 } 800 result = db.delete(tableName, selection, selectionArgs); 801 switch(match) { 802 case ACCOUNT: 803 case MAILBOX: 804 case HOSTAUTH: 805 case POLICY: 806 // Make sure all data is properly cached 807 preCacheData(); 808 break; 809 } 810 break; 811 812 default: 813 throw new IllegalArgumentException("Unknown URI " + uri); 814 } 815 if (messageDeletion) { 816 if (match == MESSAGE_ID) { 817 // Delete the Body record associated with the deleted message 818 db.execSQL(DELETE_BODY + id); 819 } else { 820 // Delete any orphaned Body records 821 db.execSQL(DELETE_ORPHAN_BODIES); 822 } 823 db.setTransactionSuccessful(); 824 } 825 } catch (SQLiteException e) { 826 checkDatabases(); 827 throw e; 828 } finally { 829 if (messageDeletion) { 830 db.endTransaction(); 831 } 832 } 833 834 // Notify all notifier cursors 835 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id); 836 837 // Notify all email content cursors 838 resolver.notifyChange(EmailContent.CONTENT_URI, null); 839 return result; 840 } 841 842 @Override 843 // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) 844 public String getType(Uri uri) { 845 int match = findMatch(uri, "getType"); 846 switch (match) { 847 case BODY_ID: 848 return "vnd.android.cursor.item/email-body"; 849 case BODY: 850 return "vnd.android.cursor.dir/email-body"; 851 case UPDATED_MESSAGE_ID: 852 case MESSAGE_ID: 853 // NOTE: According to the framework folks, we're supposed to invent mime types as 854 // a way of passing information to drag & drop recipients. 855 // If there's a mailboxId parameter in the url, we respond with a mime type that 856 // has -n appended, where n is the mailboxId of the message. The drag & drop code 857 // uses this information to know not to allow dragging the item to its own mailbox 858 String mimeType = EMAIL_MESSAGE_MIME_TYPE; 859 String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID); 860 if (mailboxId != null) { 861 mimeType += "-" + mailboxId; 862 } 863 return mimeType; 864 case UPDATED_MESSAGE: 865 case MESSAGE: 866 return "vnd.android.cursor.dir/email-message"; 867 case MAILBOX: 868 return "vnd.android.cursor.dir/email-mailbox"; 869 case MAILBOX_ID: 870 return "vnd.android.cursor.item/email-mailbox"; 871 case ACCOUNT: 872 return "vnd.android.cursor.dir/email-account"; 873 case ACCOUNT_ID: 874 return "vnd.android.cursor.item/email-account"; 875 case ATTACHMENTS_MESSAGE_ID: 876 case ATTACHMENT: 877 return "vnd.android.cursor.dir/email-attachment"; 878 case ATTACHMENT_ID: 879 return EMAIL_ATTACHMENT_MIME_TYPE; 880 case HOSTAUTH: 881 return "vnd.android.cursor.dir/email-hostauth"; 882 case HOSTAUTH_ID: 883 return "vnd.android.cursor.item/email-hostauth"; 884 default: 885 throw new IllegalArgumentException("Unknown URI " + uri); 886 } 887 } 888 889 private static final Uri UIPROVIDER_MESSAGE_NOTIFIER = 890 Uri.parse("content://" + UIProvider.AUTHORITY + "/uimessages"); 891 892 @Override 893 public Uri insert(Uri uri, ContentValues values) { 894 int match = findMatch(uri, "insert"); 895 Context context = getContext(); 896 ContentResolver resolver = context.getContentResolver(); 897 898 // See the comment at delete(), above 899 SQLiteDatabase db = getDatabase(context); 900 int table = match >> BASE_SHIFT; 901 String id = "0"; 902 long longId; 903 904 // We do NOT allow setting of unreadCount/messageCount via the provider 905 // These columns are maintained via triggers 906 if (match == MAILBOX_ID || match == MAILBOX) { 907 values.put(MailboxColumns.UNREAD_COUNT, 0); 908 values.put(MailboxColumns.MESSAGE_COUNT, 0); 909 } 910 911 Uri resultUri = null; 912 913 try { 914 switch (match) { 915 case UI_SAVEDRAFT: 916 return uiSaveDraft(uri, values); 917 case UI_SENDMAIL: 918 return uiSendMail(uri, values); 919 // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE 920 // or DELETED_MESSAGE; see the comment below for details 921 case UPDATED_MESSAGE: 922 case DELETED_MESSAGE: 923 case MESSAGE: 924 case BODY: 925 case ATTACHMENT: 926 case MAILBOX: 927 case ACCOUNT: 928 case HOSTAUTH: 929 case POLICY: 930 case QUICK_RESPONSE: 931 longId = db.insert(TABLE_NAMES[table], "foo", values); 932 resultUri = ContentUris.withAppendedId(uri, longId); 933 switch(match) { 934 case MESSAGE: 935 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 936 notifyUIProvider("Insert"); 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 notifyUIProvider("Update"); 1464 } 1465 } 1466outer: 1467 switch (match) { 1468 case UI_UPDATEDRAFT: 1469 return uiUpdateDraft(uri, values); 1470 case UI_SENDDRAFT: 1471 return uiSendDraft(uri, values); 1472 case UI_MESSAGE: 1473 return uiUpdateMessage(uri, values); 1474 case MAILBOX_ID_ADD_TO_FIELD: 1475 case ACCOUNT_ID_ADD_TO_FIELD: 1476 id = uri.getPathSegments().get(1); 1477 String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME); 1478 Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME); 1479 if (field == null || add == null) { 1480 throw new IllegalArgumentException("No field/add specified " + uri); 1481 } 1482 ContentValues actualValues = new ContentValues(); 1483 if (cache != null) { 1484 cache.lock(id); 1485 } 1486 try { 1487 db.beginTransaction(); 1488 try { 1489 Cursor c = db.query(tableName, 1490 new String[] {EmailContent.RECORD_ID, field}, 1491 whereWithId(id, selection), 1492 selectionArgs, null, null, null); 1493 try { 1494 result = 0; 1495 String[] bind = new String[1]; 1496 if (c.moveToNext()) { 1497 bind[0] = c.getString(0); // _id 1498 long value = c.getLong(1) + add; 1499 actualValues.put(field, value); 1500 result = db.update(tableName, actualValues, ID_EQUALS, bind); 1501 } 1502 db.setTransactionSuccessful(); 1503 } finally { 1504 c.close(); 1505 } 1506 } finally { 1507 db.endTransaction(); 1508 } 1509 } finally { 1510 if (cache != null) { 1511 cache.unlock(id, actualValues); 1512 } 1513 } 1514 break; 1515 case SYNCED_MESSAGE_ID: 1516 case UPDATED_MESSAGE_ID: 1517 case MESSAGE_ID: 1518 case BODY_ID: 1519 case ATTACHMENT_ID: 1520 case MAILBOX_ID: 1521 case ACCOUNT_ID: 1522 case HOSTAUTH_ID: 1523 case QUICK_RESPONSE_ID: 1524 case POLICY_ID: 1525 id = uri.getPathSegments().get(1); 1526 if (cache != null) { 1527 cache.lock(id); 1528 } 1529 try { 1530 if (match == SYNCED_MESSAGE_ID) { 1531 // For synced messages, first copy the old message to the updated table 1532 // Note the insert or ignore semantics, guaranteeing that only the first 1533 // update will be reflected in the updated message table; therefore this 1534 // row will always have the "original" data 1535 db.execSQL(UPDATED_MESSAGE_INSERT + id); 1536 } else if (match == MESSAGE_ID) { 1537 db.execSQL(UPDATED_MESSAGE_DELETE + id); 1538 } 1539 result = db.update(tableName, values, whereWithId(id, selection), 1540 selectionArgs); 1541 } catch (SQLiteException e) { 1542 // Null out values (so they aren't cached) and re-throw 1543 values = null; 1544 throw e; 1545 } finally { 1546 if (cache != null) { 1547 cache.unlock(id, values); 1548 } 1549 } 1550 if (match == ATTACHMENT_ID) { 1551 if (values.containsKey(Attachment.FLAGS)) { 1552 int flags = values.getAsInteger(Attachment.FLAGS); 1553 mAttachmentService.attachmentChanged(getContext(), 1554 Integer.parseInt(id), flags); 1555 } 1556 } 1557 break; 1558 case BODY: 1559 case MESSAGE: 1560 case UPDATED_MESSAGE: 1561 case ATTACHMENT: 1562 case MAILBOX: 1563 case ACCOUNT: 1564 case HOSTAUTH: 1565 case POLICY: 1566 switch(match) { 1567 // To avoid invalidating the cache on updates, we execute them one at a 1568 // time using the XXX_ID uri; these are all executed atomically 1569 case ACCOUNT: 1570 case MAILBOX: 1571 case HOSTAUTH: 1572 case POLICY: 1573 Cursor c = db.query(tableName, EmailContent.ID_PROJECTION, 1574 selection, selectionArgs, null, null, null); 1575 db.beginTransaction(); 1576 result = 0; 1577 try { 1578 while (c.moveToNext()) { 1579 update(ContentUris.withAppendedId( 1580 uri, c.getLong(EmailContent.ID_PROJECTION_COLUMN)), 1581 values, null, null); 1582 result++; 1583 } 1584 db.setTransactionSuccessful(); 1585 } finally { 1586 db.endTransaction(); 1587 c.close(); 1588 } 1589 break outer; 1590 // Any cached table other than those above should be invalidated here 1591 case MESSAGE: 1592 // If we're doing some generic update, the whole cache needs to be 1593 // invalidated. This case should be quite rare 1594 cache.invalidate("Update", uri, selection); 1595 //$FALL-THROUGH$ 1596 default: 1597 result = db.update(tableName, values, selection, selectionArgs); 1598 break outer; 1599 } 1600 case ACCOUNT_RESET_NEW_COUNT_ID: 1601 id = uri.getPathSegments().get(1); 1602 if (cache != null) { 1603 cache.lock(id); 1604 } 1605 ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 1606 if (values != null) { 1607 Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME); 1608 if (set != null) { 1609 newMessageCount = new ContentValues(); 1610 newMessageCount.put(Account.NEW_MESSAGE_COUNT, set); 1611 } 1612 } 1613 try { 1614 result = db.update(tableName, newMessageCount, 1615 whereWithId(id, selection), selectionArgs); 1616 } finally { 1617 if (cache != null) { 1618 cache.unlock(id, values); 1619 } 1620 } 1621 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1622 break; 1623 case ACCOUNT_RESET_NEW_COUNT: 1624 result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT, 1625 selection, selectionArgs); 1626 // Affects all accounts. Just invalidate all account cache. 1627 cache.invalidate("Reset all new counts", null, null); 1628 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1629 break; 1630 default: 1631 throw new IllegalArgumentException("Unknown URI " + uri); 1632 } 1633 } catch (SQLiteException e) { 1634 checkDatabases(); 1635 throw e; 1636 } 1637 1638 // Notify all notifier cursors 1639 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id); 1640 1641 resolver.notifyChange(notificationUri, null); 1642 return result; 1643 } 1644 1645 /** 1646 * Returns the base notification URI for the given content type. 1647 * 1648 * @param match The type of content that was modified. 1649 */ 1650 private Uri getBaseNotificationUri(int match) { 1651 Uri baseUri = null; 1652 switch (match) { 1653 case MESSAGE: 1654 case MESSAGE_ID: 1655 case SYNCED_MESSAGE_ID: 1656 baseUri = Message.NOTIFIER_URI; 1657 break; 1658 case ACCOUNT: 1659 case ACCOUNT_ID: 1660 baseUri = Account.NOTIFIER_URI; 1661 break; 1662 } 1663 return baseUri; 1664 } 1665 1666 /** 1667 * Sends a change notification to any cursors observers of the given base URI. The final 1668 * notification URI is dynamically built to contain the specified information. It will be 1669 * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending 1670 * upon the given values. 1671 * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked. 1672 * If this is necessary, it can be added. However, due to the implementation of 1673 * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications. 1674 * 1675 * @param baseUri The base URI to send notifications to. Must be able to take appended IDs. 1676 * @param op Optional operation to be appended to the URI. 1677 * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be 1678 * appended to the base URI. 1679 */ 1680 private void sendNotifierChange(Uri baseUri, String op, String id) { 1681 if (baseUri == null) return; 1682 1683 final ContentResolver resolver = getContext().getContentResolver(); 1684 1685 // Append the operation, if specified 1686 if (op != null) { 1687 baseUri = baseUri.buildUpon().appendEncodedPath(op).build(); 1688 } 1689 1690 long longId = 0L; 1691 try { 1692 longId = Long.valueOf(id); 1693 } catch (NumberFormatException ignore) {} 1694 if (longId > 0) { 1695 resolver.notifyChange(ContentUris.withAppendedId(baseUri, longId), null); 1696 } else { 1697 resolver.notifyChange(baseUri, null); 1698 } 1699 1700 // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI. 1701 if (baseUri.equals(Message.NOTIFIER_URI)) { 1702 sendMessageListDataChangedNotification(); 1703 } 1704 } 1705 1706 private void sendMessageListDataChangedNotification() { 1707 final Context context = getContext(); 1708 final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED); 1709 // Ideally this intent would contain information about which account changed, to limit the 1710 // updates to that particular account. Unfortunately, that information is not available in 1711 // sendNotifierChange(). 1712 context.sendBroadcast(intent); 1713 } 1714 1715 @Override 1716 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1717 throws OperationApplicationException { 1718 Context context = getContext(); 1719 SQLiteDatabase db = getDatabase(context); 1720 db.beginTransaction(); 1721 try { 1722 ContentProviderResult[] results = super.applyBatch(operations); 1723 db.setTransactionSuccessful(); 1724 return results; 1725 } finally { 1726 db.endTransaction(); 1727 } 1728 } 1729 1730 /** 1731 * For testing purposes, check whether a given row is cached 1732 * @param baseUri the base uri of the EmailContent 1733 * @param id the row id of the EmailContent 1734 * @return whether or not the row is currently cached 1735 */ 1736 @VisibleForTesting 1737 protected boolean isCached(Uri baseUri, long id) { 1738 int match = findMatch(baseUri, "isCached"); 1739 int table = match >> BASE_SHIFT; 1740 ContentCache cache = mContentCaches[table]; 1741 if (cache == null) return false; 1742 Cursor cc = cache.get(Long.toString(id)); 1743 return (cc != null); 1744 } 1745 1746 public static interface AttachmentService { 1747 /** 1748 * Notify the service that an attachment has changed. 1749 */ 1750 void attachmentChanged(Context context, long id, int flags); 1751 } 1752 1753 private final AttachmentService DEFAULT_ATTACHMENT_SERVICE = new AttachmentService() { 1754 @Override 1755 public void attachmentChanged(Context context, long id, int flags) { 1756 // The default implementation delegates to the real service. 1757 AttachmentDownloadService.attachmentChanged(context, id, flags); 1758 } 1759 }; 1760 private AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE; 1761 1762 /** 1763 * Injects a custom attachment service handler. If null is specified, will reset to the 1764 * default service. 1765 */ 1766 public void injectAttachmentService(AttachmentService as) { 1767 mAttachmentService = (as == null) ? DEFAULT_ATTACHMENT_SERVICE : as; 1768 } 1769 1770 /** 1771 * Support for UnifiedEmail below 1772 */ 1773 1774 private static final String NOT_A_DRAFT_STRING = 1775 Integer.toString(UIProvider.DraftType.NOT_A_DRAFT); 1776 1777 /** 1778 * Mapping of UIProvider columns to EmailProvider columns for the message list (called the 1779 * conversation list in UnifiedEmail) 1780 */ 1781 private static final ProjectionMap sMessageListMap = ProjectionMap.builder() 1782 .add(BaseColumns._ID, MessageColumns.ID) 1783 .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage")) 1784 .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage")) 1785 .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT) 1786 .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET) 1787 .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST) 1788 .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) 1789 .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) 1790 .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1") 1791 .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0") 1792 .add(UIProvider.ConversationColumns.SENDING_STATE, 1793 Integer.toString(ConversationSendingState.OTHER)) 1794 .add(UIProvider.ConversationColumns.PRIORITY, Integer.toString(ConversationPriority.LOW)) 1795 .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ) 1796 .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE) 1797 .add(UIProvider.ConversationColumns.FOLDER_LIST, MessageColumns.MAILBOX_KEY) 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 String getFolderCapabilities() { 1838 return "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL + 1839 ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES + 1840 " ELSE 0 END"; 1841 } 1842 1843 private static final ProjectionMap sFolderListMap = ProjectionMap.builder() 1844 .add(BaseColumns._ID, MessageColumns.ID) 1845 .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) 1846 .add(UIProvider.FolderColumns.NAME, "displayName") 1847 .add(UIProvider.FolderColumns.HAS_CHILDREN, 1848 MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN) 1849 .add(UIProvider.FolderColumns.CAPABILITIES, getFolderCapabilities()) 1850 .add(UIProvider.FolderColumns.SYNC_FREQUENCY, "0") 1851 .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") 1852 .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) 1853 .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("subfolders")) 1854 .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT) 1855 .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.MESSAGE_COUNT) 1856 .build(); 1857 1858 /** 1859 * The "ORDER BY" clause for top level folders 1860 */ 1861 private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE 1862 + " WHEN " + Mailbox.TYPE_INBOX + " THEN 0" 1863 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN 1" 1864 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN 2" 1865 + " WHEN " + Mailbox.TYPE_SENT + " THEN 3" 1866 + " WHEN " + Mailbox.TYPE_TRASH + " THEN 4" 1867 + " WHEN " + Mailbox.TYPE_JUNK + " THEN 5" 1868 // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order. 1869 + " ELSE 10 END" 1870 + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 1871 1872 /** 1873 * Generate the SELECT clause using a specified mapping and the original UI projection 1874 * @param map the ProjectionMap to use for this projection 1875 * @param projection the projection as sent by UnifiedEmail 1876 * @return a StringBuilder containing the SELECT expression for a SQLite query 1877 */ 1878 private StringBuilder genSelect(ProjectionMap map, String[] projection) { 1879 StringBuilder sb = new StringBuilder("SELECT "); 1880 boolean first = true; 1881 for (String column: projection) { 1882 if (first) { 1883 first = false; 1884 } else { 1885 sb.append(','); 1886 } 1887 String val = map.get(column); 1888 // If we don't have the column, be permissive, returning "0 AS <column>", and warn 1889 if (val == null) { 1890 Log.w(TAG, "UIProvider column not found, returning 0: " + column); 1891 val = "0 AS " + column; 1892 } 1893 sb.append(val); 1894 } 1895 return sb; 1896 } 1897 1898 /** 1899 * Convenience method to create a Uri string given the "type" of query; we append the type 1900 * of the query and the id column name (_id) 1901 * 1902 * @param type the "type" of the query, as defined by our UriMatcher definitions 1903 * @return a Uri string 1904 */ 1905 private static String uriWithId(String type) { 1906 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || _id"; 1907 } 1908 1909 /** 1910 * Convenience method to create a Uri string given the "type" of query and the table name to 1911 * which it applies; we append the type of the query and the fully qualified (FQ) id column 1912 * (i.e. including the table name); we need this for join queries where _id would otherwise 1913 * be ambiguous 1914 * 1915 * @param type the "type" of the query, as defined by our UriMatcher definitions 1916 * @param tableName the name of the table whose _id is referred to 1917 * @return a Uri string 1918 */ 1919 private static String uriWithFQId(String type, String tableName) { 1920 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id"; 1921 } 1922 1923 /** 1924 * Generate the "view message" SQLite query, given a projection from UnifiedEmail 1925 * 1926 * @param uiProjection as passed from UnifiedEmail 1927 * @return the SQLite query to be executed on the EmailProvider database 1928 */ 1929 private String genQueryViewMessage(String[] uiProjection) { 1930 StringBuilder sb = genSelect(sMessageViewMap, uiProjection); 1931 sb.append(" FROM " + Message.TABLE_NAME + "," + Body.TABLE_NAME + " WHERE " + 1932 Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " AND " + 1933 Message.TABLE_NAME + "." + Message.RECORD_ID + "=?"); 1934 return sb.toString(); 1935 } 1936 1937 /** 1938 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 1939 * 1940 * @param uiProjection as passed from UnifiedEmail 1941 * @return the SQLite query to be executed on the EmailProvider database 1942 */ 1943 private String genQueryMailboxMessages(String[] uiProjection) { 1944 StringBuilder sb = genSelect(sMessageListMap, uiProjection); 1945 // Make constant 1946 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.MAILBOX_KEY + "=? ORDER BY " + 1947 MessageColumns.TIMESTAMP + " DESC"); 1948 return sb.toString(); 1949 } 1950 1951 /** 1952 * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail 1953 * 1954 * @param uiProjection as passed from UnifiedEmail 1955 * @return the SQLite query to be executed on the EmailProvider database 1956 */ 1957 private String genQueryAccountMailboxes(String[] uiProjection) { 1958 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 1959 // Make constant 1960 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 1961 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 1962 " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY "); 1963 sb.append(MAILBOX_ORDER_BY); 1964 return sb.toString(); 1965 } 1966 1967 /** 1968 * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail 1969 * 1970 * @param uiProjection as passed from UnifiedEmail 1971 * @return the SQLite query to be executed on the EmailProvider database 1972 */ 1973 private String genQuerySubfolders(String[] uiProjection) { 1974 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 1975 // Make constant 1976 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY + 1977 " =? ORDER BY "); 1978 sb.append(MAILBOX_ORDER_BY); 1979 return sb.toString(); 1980 } 1981 1982 /** 1983 * Given the email address of an account, return its account id (the _id row in the Account 1984 * table), or NO_ACCOUNT (-1) if not found 1985 * 1986 * @param email the email address of the account 1987 * @return the account id for this account, or NO_ACCOUNT if not found 1988 */ 1989 private long findAccountIdByName(String email) { 1990 Map<String, Cursor> accountCache = mCacheAccount.getSnapshot(); 1991 Collection<Cursor> accounts = accountCache.values(); 1992 for (Cursor accountCursor: accounts) { 1993 if (accountCursor.getString(Account.CONTENT_EMAIL_ADDRESS_COLUMN).equals(email)) { 1994 return accountCursor.getLong(Account.CONTENT_ID_COLUMN); 1995 } 1996 } 1997 return Account.NO_ACCOUNT; 1998 } 1999 2000 /** 2001 * Handle UnifiedEmail queries here (dispatched from query()) 2002 * 2003 * @param match the UriMatcher match for the original uri passed in from UnifiedEmail 2004 * @param uri the original uri passed in from UnifiedEmail 2005 * @param uiProjection the projection passed in from UnifiedEmail 2006 * @return the result Cursor 2007 */ 2008 private Cursor uiQuery(int match, Uri uri, String[] uiProjection) { 2009 Context context = getContext(); 2010 SQLiteDatabase db = getDatabase(context); 2011 // Should we ever return null, or throw an exception?? 2012 Cursor c = null; 2013 String id = uri.getPathSegments().get(1); 2014 switch(match) { 2015 case UI_FOLDERS: 2016 // We are passed the email address (unique account identifier) in the uri; we 2017 // need to turn this into the _id of the Account row in the EmailProvider db 2018 String accountName = id; 2019 long acctId = findAccountIdByName(accountName); 2020 if (acctId == Account.NO_ACCOUNT) return null; 2021 c = db.rawQuery(genQueryAccountMailboxes(uiProjection), 2022 new String[] {Long.toString(acctId)}); 2023 break; 2024 case UI_SUBFOLDERS: 2025 c = db.rawQuery(genQuerySubfolders(uiProjection), new String[] {id}); 2026 break; 2027 case UI_MESSAGES: 2028 c = db.rawQuery(genQueryMailboxMessages(uiProjection), new String[] {id}); 2029 break; 2030 case UI_MESSAGE: 2031 c = db.rawQuery(genQueryViewMessage(uiProjection), new String[] {id}); 2032 break; 2033 } 2034 if (c != null) { 2035 // Notify UIProvider on changes 2036 // Make this more specific to actual query later on... 2037 c.setNotificationUri(context.getContentResolver(), UIPROVIDER_MESSAGE_NOTIFIER); 2038 } 2039 return c; 2040 } 2041 2042 /** 2043 * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need 2044 * a few of the fields 2045 * @param uiAtt the UIProvider attachment to convert 2046 * @return the EmailProvider attachment 2047 */ 2048 private Attachment convertUiAttachmentToAttachment( 2049 com.android.mail.providers.Attachment uiAtt) { 2050 Attachment att = new Attachment(); 2051 att.mContentUri = uiAtt.contentUri; 2052 att.mFileName = uiAtt.name; 2053 att.mMimeType = uiAtt.mimeType; 2054 att.mSize = uiAtt.size; 2055 return att; 2056 } 2057 2058 /** 2059 * Create a mailbox given the account and mailboxType. 2060 */ 2061 private Mailbox createMailbox(long accountId, int mailboxType) { 2062 Context context = getContext(); 2063 int resId = -1; 2064 switch (mailboxType) { 2065 case Mailbox.TYPE_INBOX: 2066 resId = R.string.mailbox_name_server_inbox; 2067 break; 2068 case Mailbox.TYPE_OUTBOX: 2069 resId = R.string.mailbox_name_server_outbox; 2070 break; 2071 case Mailbox.TYPE_DRAFTS: 2072 resId = R.string.mailbox_name_server_drafts; 2073 break; 2074 case Mailbox.TYPE_TRASH: 2075 resId = R.string.mailbox_name_server_trash; 2076 break; 2077 case Mailbox.TYPE_SENT: 2078 resId = R.string.mailbox_name_server_sent; 2079 break; 2080 case Mailbox.TYPE_JUNK: 2081 resId = R.string.mailbox_name_server_junk; 2082 break; 2083 default: 2084 throw new IllegalArgumentException("Illegal mailbox type"); 2085 } 2086 Log.d(TAG, "Creating mailbox of type " + mailboxType + " for account " + accountId); 2087 Mailbox box = Mailbox.newSystemMailbox(accountId, mailboxType, context.getString(resId)); 2088 box.save(context); 2089 return box; 2090 } 2091 2092 /** 2093 * Given an account name and a mailbox type, return that mailbox, creating it if necessary 2094 * @param accountName the account name to use 2095 * @param mailboxType the type of mailbox we're trying to find 2096 * @return the mailbox of the given type for the account in the uri, or null if not found 2097 */ 2098 private Mailbox getMailboxByUriAndType(String accountName, int mailboxType) { 2099 long accountId = findAccountIdByName(accountName); 2100 if (accountId == Account.NO_ACCOUNT) return null; 2101 Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), accountId, mailboxType); 2102 if (mailbox == null) { 2103 mailbox = createMailbox(accountId, mailboxType); 2104 } 2105 return mailbox; 2106 } 2107 2108 private Message getMessageFromPathSegments(List<String> pathSegments) { 2109 Message msg = null; 2110 if (pathSegments.size() > 2) { 2111 msg = Message.restoreMessageWithId(getContext(), Long.parseLong(pathSegments.get(2))); 2112 } 2113 if (msg == null) { 2114 msg = new Message(); 2115 } 2116 return msg; 2117 } 2118 /** 2119 * Given a mailbox and the content values for a message, create/save the message in the mailbox 2120 * @param mailbox the mailbox to use 2121 * @param values the content values that represent message fields 2122 * @return the uri of the newly created message 2123 */ 2124 private Uri uiSaveMessage(Message msg, Mailbox mailbox, ContentValues values) { 2125 Context context = getContext(); 2126 // Fill in the message 2127 msg.mTo = values.getAsString(UIProvider.MessageColumns.TO); 2128 msg.mCc = values.getAsString(UIProvider.MessageColumns.CC); 2129 msg.mBcc = values.getAsString(UIProvider.MessageColumns.BCC); 2130 msg.mSubject = values.getAsString(UIProvider.MessageColumns.SUBJECT); 2131 msg.mText = values.getAsString(UIProvider.MessageColumns.BODY_TEXT); 2132 msg.mHtml = values.getAsString(UIProvider.MessageColumns.BODY_HTML); 2133 msg.mMailboxKey = mailbox.mId; 2134 msg.mAccountKey = mailbox.mAccountKey; 2135 msg.mDisplayName = msg.mTo; 2136 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 2137 // Get attachments from the ContentValues 2138 ArrayList<com.android.mail.providers.Attachment> uiAtts = 2139 com.android.mail.providers.Attachment.getAttachmentsFromJoinedAttachmentInfo( 2140 values.getAsString(UIProvider.MessageColumns.JOINED_ATTACHMENT_INFOS)); 2141 ArrayList<Attachment> atts = new ArrayList<Attachment>(); 2142 for (com.android.mail.providers.Attachment uiAtt: uiAtts) { 2143 // Convert to our attachments and add to the list; everything else should "just work" 2144 atts.add(convertUiAttachmentToAttachment(uiAtt)); 2145 } 2146 if (!atts.isEmpty()) { 2147 msg.mAttachments = atts; 2148 } 2149 // Save it or update it... 2150 if (!msg.isSaved()) { 2151 msg.save(context); 2152 } else { 2153 // This is tricky due to how messages/attachments are saved; rather than putz with 2154 // what's changed, we'll delete/re-add them 2155 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 2156 // Delete all existing attachments 2157 ops.add(ContentProviderOperation.newDelete( 2158 ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId)) 2159 .build()); 2160 // Delete the body 2161 ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI) 2162 .withSelection(Body.MESSAGE_KEY + "=?", new String[] {Long.toString(msg.mId)}) 2163 .build()); 2164 // Add the ops for the message, atts, and body 2165 msg.addSaveOps(ops); 2166 // Do it! 2167 try { 2168 applyBatch(ops); 2169 } catch (OperationApplicationException e) { 2170 } 2171 } 2172 return Uri.parse("content://" + EmailContent.AUTHORITY + "/uimessage/" + msg.mId); 2173 } 2174 2175 /** 2176 * Create and send the message via the account indicated in the uri 2177 * @param uri the incoming uri 2178 * @param values the content values that represent message fields 2179 * @return the uri of the created message 2180 */ 2181 private Uri uiSendMail(Uri uri, ContentValues values) { 2182 List<String> pathSegments = uri.getPathSegments(); 2183 Mailbox mailbox = getMailboxByUriAndType(pathSegments.get(1), Mailbox.TYPE_OUTBOX); 2184 if (mailbox == null) return null; 2185 Message msg = getMessageFromPathSegments(pathSegments); 2186 try { 2187 return uiSaveMessage(msg, mailbox, values); 2188 } finally { 2189 // Kick observers 2190 getContext().getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 2191 } 2192 } 2193 2194 /** 2195 * Create a message and save it to the drafts folder of the account indicated in the uri 2196 * @param uri the incoming uri 2197 * @param values the content values that represent message fields 2198 * @return the uri of the created message 2199 */ 2200 private Uri uiSaveDraft(Uri uri, ContentValues values) { 2201 List<String> pathSegments = uri.getPathSegments(); 2202 Mailbox mailbox = getMailboxByUriAndType(pathSegments.get(1), Mailbox.TYPE_DRAFTS); 2203 if (mailbox == null) return null; 2204 Message msg = getMessageFromPathSegments(pathSegments); 2205 return uiSaveMessage(msg, mailbox, values); 2206 } 2207 2208 private int uiUpdateDraft(Uri uri, ContentValues values) { 2209 Context context = getContext(); 2210 Message msg = Message.restoreMessageWithId(context, 2211 Long.parseLong(uri.getPathSegments().get(1))); 2212 if (msg == null) return 0; 2213 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 2214 if (mailbox == null) return 0; 2215 uiSaveMessage(msg, mailbox, values); 2216 return 1; 2217 } 2218 2219 private int uiSendDraft(Uri uri, ContentValues values) { 2220 Context context = getContext(); 2221 Message msg = Message.restoreMessageWithId(context, 2222 Long.parseLong(uri.getPathSegments().get(1))); 2223 if (msg == null) return 0; 2224 long mailboxId = Mailbox.findMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_OUTBOX); 2225 if (mailboxId == Mailbox.NO_MAILBOX) return 0; 2226 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 2227 if (mailbox == null) return 0; 2228 uiSaveMessage(msg, mailbox, values); 2229 // Kick observers 2230 context.getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 2231 return 1; 2232 } 2233 2234 private void putIntegerLongOrBoolean(ContentValues values, String columnName, Object value) { 2235 if (value instanceof Integer) { 2236 Integer intValue = (Integer)value; 2237 values.put(columnName, intValue); 2238 } else if (value instanceof Boolean) { 2239 Boolean boolValue = (Boolean)value; 2240 values.put(columnName, boolValue ? 1 : 0); 2241 } else if (value instanceof Long) { 2242 Long longValue = (Long)value; 2243 values.put(columnName, longValue); 2244 } 2245 } 2246 2247 private ContentValues convertUiMessageValues(ContentValues values) { 2248 ContentValues ourValues = new ContentValues(); 2249 for (String columnName: values.keySet()) { 2250 Object val = values.get(columnName); 2251 if (columnName.equals(UIProvider.ConversationColumns.STARRED)) { 2252 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val); 2253 } else if (columnName.equals(UIProvider.ConversationColumns.READ)) { 2254 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val); 2255 } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 2256 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val); 2257 } else if (columnName.equals(UIProvider.ConversationColumns.FOLDER_LIST)) { 2258 // Convert from folder list uri to mailbox key 2259 Uri uri = Uri.parse((String)val); 2260 Long mailboxId = Long.parseLong(uri.getLastPathSegment()); 2261 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId); 2262 } else { 2263 throw new IllegalArgumentException("Can't update " + columnName + " in message"); 2264 } 2265 } 2266 return ourValues; 2267 } 2268 2269 private Uri convertToEmailProviderUri(Uri uri, boolean asProvider) { 2270 String idString = uri.getLastPathSegment(); 2271 try { 2272 long id = Long.parseLong(idString); 2273 Uri ourUri = ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id); 2274 if (asProvider) { 2275 ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build(); 2276 } 2277 return ourUri; 2278 } catch (NumberFormatException e) { 2279 return null; 2280 } 2281 } 2282 2283 private Message getMessageFromLastSegment(Uri uri) { 2284 long messageId = Long.parseLong(uri.getLastPathSegment()); 2285 return Message.restoreMessageWithId(getContext(), messageId); 2286 } 2287 2288 /** 2289 * Add an undo operation for the current sequence; if the sequence is newer than what we've had, 2290 * clear out the undo list and start over 2291 * @param uri the uri we're working on 2292 * @param op the ContentProviderOperation to perform upon undo 2293 */ 2294 private void addToSequence(Uri uri, ContentProviderOperation op) { 2295 String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER); 2296 if (sequenceString != null) { 2297 int sequence = Integer.parseInt(sequenceString); 2298 if (sequence > mLastSequence) { 2299 // Reset sequence 2300 mLastSequenceOps.clear(); 2301 mLastSequence = sequence; 2302 } 2303 // TODO: Need something to indicate a change isn't ready (undoable) 2304 mLastSequenceOps.add(op); 2305 } 2306 } 2307 2308 private int uiUpdateMessage(Uri uri, ContentValues values) { 2309 Uri ourUri = convertToEmailProviderUri(uri, true); 2310 if (ourUri == null) return 0; 2311 ContentValues ourValues = convertUiMessageValues(values); 2312 Message msg = getMessageFromLastSegment(uri); 2313 if (msg == null) return 0; 2314 ContentValues undoValues = new ContentValues(); 2315 for (String columnName: ourValues.keySet()) { 2316 if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 2317 undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey); 2318 } else if (columnName.equals(MessageColumns.FLAG_READ)) { 2319 undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead); 2320 } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) { 2321 undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite); 2322 } 2323 } 2324 ContentProviderOperation op = 2325 ContentProviderOperation.newUpdate(convertToEmailProviderUri(uri, false)) 2326 .withValues(undoValues) 2327 .build(); 2328 addToSequence(uri, op); 2329 return update(ourUri, ourValues, null, null); 2330 } 2331 2332 private int uiDeleteMessage(Uri uri) { 2333 Context context = getContext(); 2334 Message msg = getMessageFromLastSegment(uri); 2335 if (msg == null) return 0; 2336 Mailbox mailbox = 2337 Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH); 2338 if (mailbox == null) return 0; 2339 ContentProviderOperation op = 2340 ContentProviderOperation.newUpdate(convertToEmailProviderUri(uri, false)) 2341 .withValue(Message.MAILBOX_KEY, msg.mMailboxKey) 2342 .build(); 2343 addToSequence(uri, op); 2344 ContentValues values = new ContentValues(); 2345 values.put(Message.MAILBOX_KEY, mailbox.mId); 2346 return uiUpdateMessage(uri, values); 2347 } 2348 2349 private Cursor uiUndo(Uri uri, String[] projection) { 2350 // First see if we have any operations saved 2351 // TODO: Make sure seq matches 2352 if (!mLastSequenceOps.isEmpty()) { 2353 try { 2354 // TODO Always use this projection? Or what's passed in? 2355 // Not sure if UI wants it, but I'm making a cursor of convo uri's 2356 MatrixCursor c = new MatrixCursor( 2357 new String[] {UIProvider.ConversationColumns.URI}, 2358 mLastSequenceOps.size()); 2359 for (ContentProviderOperation op: mLastSequenceOps) { 2360 c.addRow(new String[] {op.getUri().toString()}); 2361 } 2362 // Just apply the batch and we're done! 2363 applyBatch(mLastSequenceOps); 2364 // But clear the operations 2365 mLastSequenceOps.clear(); 2366 // Tell the UI there are changes 2367 notifyUIProvider("Undo"); 2368 return c; 2369 } catch (OperationApplicationException e) { 2370 } 2371 } 2372 return new MatrixCursor(projection, 0); 2373 } 2374 2375 private void notifyUIProvider(String reason) { 2376 getContext().getContentResolver().notifyChange(UIPROVIDER_MESSAGE_NOTIFIER, null); 2377 // Temporary 2378 Log.d(TAG, "[Notify UIProvider " + reason + "]"); 2379 } 2380} 2381