EmailProvider.java revision d57e096b2cf9c85a3c2df733f92e7f8b2d26402a
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_SUBFOLDERS: 1109 case UI_FOLDERS: 1110 case UI_MESSAGES: 1111 case UI_MESSAGE: 1112 // For now, we don't allow selection criteria within these queries 1113 if (selection != null || selectionArgs != null) { 1114 throw new IllegalArgumentException("UI queries can't have selection/args"); 1115 } 1116 c = uiQuery(match, uri, projection); 1117 return c; 1118 case ACCOUNT_DEFAULT_ID: 1119 // Start with a snapshot of the cache 1120 Map<String, Cursor> accountCache = mCacheAccount.getSnapshot(); 1121 long accountId = Account.NO_ACCOUNT; 1122 // Find the account with "isDefault" set, or the lowest account ID otherwise. 1123 // Note that the snapshot from the cached isn't guaranteed to be sorted in any 1124 // way. 1125 Collection<Cursor> accounts = accountCache.values(); 1126 for (Cursor accountCursor: accounts) { 1127 // For now, at least, we can have zero count cursors (e.g. if someone looks 1128 // up a non-existent id); we need to skip these 1129 if (accountCursor.moveToFirst()) { 1130 boolean isDefault = 1131 accountCursor.getInt(Account.CONTENT_IS_DEFAULT_COLUMN) == 1; 1132 long iterId = accountCursor.getLong(Account.CONTENT_ID_COLUMN); 1133 // We'll remember this one if it's the default or the first one we see 1134 if (isDefault) { 1135 accountId = iterId; 1136 break; 1137 } else if ((accountId == Account.NO_ACCOUNT) || (iterId < accountId)) { 1138 accountId = iterId; 1139 } 1140 } 1141 } 1142 // Return a cursor with an id projection 1143 MatrixCursor mc = new MatrixCursor(EmailContent.ID_PROJECTION); 1144 mc.addRow(new Object[] {accountId}); 1145 c = mc; 1146 break; 1147 case MAILBOX_ID_FROM_ACCOUNT_AND_TYPE: 1148 // Get accountId and type and find the mailbox in our map 1149 List<String> pathSegments = uri.getPathSegments(); 1150 accountId = Long.parseLong(pathSegments.get(1)); 1151 int type = Integer.parseInt(pathSegments.get(2)); 1152 long mailboxId = getMailboxIdFromMailboxTypeMap(accountId, type); 1153 // Return a cursor with an id projection 1154 mc = new MatrixCursor(EmailContent.ID_PROJECTION); 1155 mc.addRow(new Object[] {mailboxId}); 1156 c = mc; 1157 break; 1158 case BODY: 1159 case MESSAGE: 1160 case UPDATED_MESSAGE: 1161 case DELETED_MESSAGE: 1162 case ATTACHMENT: 1163 case MAILBOX: 1164 case ACCOUNT: 1165 case HOSTAUTH: 1166 case POLICY: 1167 case QUICK_RESPONSE: 1168 // Special-case "count of accounts"; it's common and we always know it 1169 if (match == ACCOUNT && Arrays.equals(projection, EmailContent.COUNT_COLUMNS) && 1170 selection == null && limit.equals("1")) { 1171 int accountCount = mMailboxTypeMap.size(); 1172 // In the rare case there are MAX_CACHED_ACCOUNTS or more, we can't do this 1173 if (accountCount < MAX_CACHED_ACCOUNTS) { 1174 mc = new MatrixCursor(projection, 1); 1175 mc.addRow(new Object[] {accountCount}); 1176 c = mc; 1177 break; 1178 } 1179 } 1180 c = db.query(tableName, projection, 1181 selection, selectionArgs, null, null, sortOrder, limit); 1182 break; 1183 case BODY_ID: 1184 case MESSAGE_ID: 1185 case DELETED_MESSAGE_ID: 1186 case UPDATED_MESSAGE_ID: 1187 case ATTACHMENT_ID: 1188 case MAILBOX_ID: 1189 case ACCOUNT_ID: 1190 case HOSTAUTH_ID: 1191 case POLICY_ID: 1192 case QUICK_RESPONSE_ID: 1193 id = uri.getPathSegments().get(1); 1194 if (cache != null) { 1195 c = cache.getCachedCursor(id, projection); 1196 } 1197 if (c == null) { 1198 CacheToken token = null; 1199 if (cache != null) { 1200 token = cache.getCacheToken(id); 1201 } 1202 c = db.query(tableName, projection, whereWithId(id, selection), 1203 selectionArgs, null, null, sortOrder, limit); 1204 if (cache != null) { 1205 c = cache.putCursor(c, id, projection, token); 1206 } 1207 } 1208 break; 1209 case ATTACHMENTS_MESSAGE_ID: 1210 // All attachments for the given message 1211 id = uri.getPathSegments().get(2); 1212 c = db.query(Attachment.TABLE_NAME, projection, 1213 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), 1214 selectionArgs, null, null, sortOrder, limit); 1215 break; 1216 case QUICK_RESPONSE_ACCOUNT_ID: 1217 // All quick responses for the given account 1218 id = uri.getPathSegments().get(2); 1219 c = db.query(QuickResponse.TABLE_NAME, projection, 1220 whereWith(QuickResponse.ACCOUNT_KEY + "=" + id, selection), 1221 selectionArgs, null, null, sortOrder); 1222 break; 1223 default: 1224 throw new IllegalArgumentException("Unknown URI " + uri); 1225 } 1226 } catch (SQLiteException e) { 1227 checkDatabases(); 1228 throw e; 1229 } catch (RuntimeException e) { 1230 checkDatabases(); 1231 e.printStackTrace(); 1232 throw e; 1233 } finally { 1234 if (cache != null && c != null && Email.DEBUG) { 1235 cache.recordQueryTime(c, System.nanoTime() - time); 1236 } 1237 if (c == null) { 1238 // This should never happen, but let's be sure to log it... 1239 Log.e(TAG, "Query returning null for uri: " + uri + ", selection: " + selection); 1240 } 1241 } 1242 1243 if ((c != null) && !isTemporary()) { 1244 c.setNotificationUri(getContext().getContentResolver(), uri); 1245 } 1246 return c; 1247 } 1248 1249 private String whereWithId(String id, String selection) { 1250 StringBuilder sb = new StringBuilder(256); 1251 sb.append("_id="); 1252 sb.append(id); 1253 if (selection != null) { 1254 sb.append(" AND ("); 1255 sb.append(selection); 1256 sb.append(')'); 1257 } 1258 return sb.toString(); 1259 } 1260 1261 /** 1262 * Combine a locally-generated selection with a user-provided selection 1263 * 1264 * This introduces risk that the local selection might insert incorrect chars 1265 * into the SQL, so use caution. 1266 * 1267 * @param where locally-generated selection, must not be null 1268 * @param selection user-provided selection, may be null 1269 * @return a single selection string 1270 */ 1271 private String whereWith(String where, String selection) { 1272 if (selection == null) { 1273 return where; 1274 } 1275 StringBuilder sb = new StringBuilder(where); 1276 sb.append(" AND ("); 1277 sb.append(selection); 1278 sb.append(')'); 1279 1280 return sb.toString(); 1281 } 1282 1283 /** 1284 * Restore a HostAuth from a database, given its unique id 1285 * @param db the database 1286 * @param id the unique id (_id) of the row 1287 * @return a fully populated HostAuth or null if the row does not exist 1288 */ 1289 private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) { 1290 Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION, 1291 HostAuth.RECORD_ID + "=?", new String[] {Long.toString(id)}, null, null, null); 1292 try { 1293 if (c.moveToFirst()) { 1294 HostAuth hostAuth = new HostAuth(); 1295 hostAuth.restore(c); 1296 return hostAuth; 1297 } 1298 return null; 1299 } finally { 1300 c.close(); 1301 } 1302 } 1303 1304 /** 1305 * Copy the Account and HostAuth tables from one database to another 1306 * @param fromDatabase the source database 1307 * @param toDatabase the destination database 1308 * @return the number of accounts copied, or -1 if an error occurred 1309 */ 1310 private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) { 1311 if (fromDatabase == null || toDatabase == null) return -1; 1312 int copyCount = 0; 1313 try { 1314 // Lock both databases; for the "from" database, we don't want anyone changing it from 1315 // under us; for the "to" database, we want to make the operation atomic 1316 fromDatabase.beginTransaction(); 1317 toDatabase.beginTransaction(); 1318 // Delete anything hanging around here 1319 toDatabase.delete(Account.TABLE_NAME, null, null); 1320 toDatabase.delete(HostAuth.TABLE_NAME, null, null); 1321 // Get our account cursor 1322 Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION, 1323 null, null, null, null, null); 1324 boolean noErrors = true; 1325 try { 1326 // Loop through accounts, copying them and associated host auth's 1327 while (c.moveToNext()) { 1328 Account account = new Account(); 1329 account.restore(c); 1330 1331 // Clear security sync key and sync key, as these were specific to the state of 1332 // the account, and we've reset that... 1333 // Clear policy key so that we can re-establish policies from the server 1334 // TODO This is pretty EAS specific, but there's a lot of that around 1335 account.mSecuritySyncKey = null; 1336 account.mSyncKey = null; 1337 account.mPolicyKey = 0; 1338 1339 // Copy host auth's and update foreign keys 1340 HostAuth hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeyRecv); 1341 // The account might have gone away, though very unlikely 1342 if (hostAuth == null) continue; 1343 account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null, 1344 hostAuth.toContentValues()); 1345 // EAS accounts have no send HostAuth 1346 if (account.mHostAuthKeySend > 0) { 1347 hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend); 1348 // Belt and suspenders; I can't imagine that this is possible, since we 1349 // checked the validity of the account above, and the database is now locked 1350 if (hostAuth == null) continue; 1351 account.mHostAuthKeySend = toDatabase.insert(HostAuth.TABLE_NAME, null, 1352 hostAuth.toContentValues()); 1353 } 1354 // Now, create the account in the "to" database 1355 toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues()); 1356 copyCount++; 1357 } 1358 } catch (SQLiteException e) { 1359 noErrors = false; 1360 copyCount = -1; 1361 } finally { 1362 fromDatabase.endTransaction(); 1363 if (noErrors) { 1364 // Say it's ok to commit 1365 toDatabase.setTransactionSuccessful(); 1366 } 1367 toDatabase.endTransaction(); 1368 c.close(); 1369 } 1370 } catch (SQLiteException e) { 1371 copyCount = -1; 1372 } 1373 return copyCount; 1374 } 1375 1376 private static SQLiteDatabase getBackupDatabase(Context context) { 1377 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, BACKUP_DATABASE_NAME); 1378 return helper.getWritableDatabase(); 1379 } 1380 1381 /** 1382 * Backup account data, returning the number of accounts backed up 1383 */ 1384 private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) { 1385 if (Email.DEBUG) { 1386 Log.d(TAG, "backupAccounts..."); 1387 } 1388 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1389 try { 1390 int numBackedUp = copyAccountTables(mainDatabase, backupDatabase); 1391 if (numBackedUp < 0) { 1392 Log.e(TAG, "Account backup failed!"); 1393 } else if (Email.DEBUG) { 1394 Log.d(TAG, "Backed up " + numBackedUp + " accounts..."); 1395 } 1396 return numBackedUp; 1397 } finally { 1398 if (backupDatabase != null) { 1399 backupDatabase.close(); 1400 } 1401 } 1402 } 1403 1404 /** 1405 * Restore account data, returning the number of accounts restored 1406 */ 1407 private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) { 1408 if (Email.DEBUG) { 1409 Log.d(TAG, "restoreAccounts..."); 1410 } 1411 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1412 try { 1413 int numRecovered = copyAccountTables(backupDatabase, mainDatabase); 1414 if (numRecovered > 0) { 1415 Log.e(TAG, "Recovered " + numRecovered + " accounts!"); 1416 } else if (numRecovered < 0) { 1417 Log.e(TAG, "Account recovery failed?"); 1418 } else if (Email.DEBUG) { 1419 Log.d(TAG, "No accounts to restore..."); 1420 } 1421 return numRecovered; 1422 } finally { 1423 if (backupDatabase != null) { 1424 backupDatabase.close(); 1425 } 1426 } 1427 } 1428 1429 @Override 1430 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1431 // Handle this special case the fastest possible way 1432 if (uri == INTEGRITY_CHECK_URI) { 1433 checkDatabases(); 1434 return 0; 1435 } else if (uri == ACCOUNT_BACKUP_URI) { 1436 return backupAccounts(getContext(), getDatabase(getContext())); 1437 } 1438 1439 // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID) 1440 Uri notificationUri = EmailContent.CONTENT_URI; 1441 1442 int match = findMatch(uri, "update"); 1443 Context context = getContext(); 1444 ContentResolver resolver = context.getContentResolver(); 1445 // See the comment at delete(), above 1446 SQLiteDatabase db = getDatabase(context); 1447 int table = match >> BASE_SHIFT; 1448 int result; 1449 1450 // We do NOT allow setting of unreadCount/messageCount via the provider 1451 // These columns are maintained via triggers 1452 if (match == MAILBOX_ID || match == MAILBOX) { 1453 values.remove(MailboxColumns.UNREAD_COUNT); 1454 values.remove(MailboxColumns.MESSAGE_COUNT); 1455 } 1456 1457 ContentCache cache = mContentCaches[table]; 1458 String tableName = TABLE_NAMES[table]; 1459 String id = "0"; 1460 1461 try { 1462 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 1463 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 1464 notifyUIProvider("Update"); 1465 } 1466 } 1467outer: 1468 switch (match) { 1469 case UI_UPDATEDRAFT: 1470 return uiUpdateDraft(uri, values); 1471 case UI_SENDDRAFT: 1472 return uiSendDraft(uri, values); 1473 case UI_MESSAGE: 1474 return uiUpdateMessage(uri, values); 1475 case MAILBOX_ID_ADD_TO_FIELD: 1476 case ACCOUNT_ID_ADD_TO_FIELD: 1477 id = uri.getPathSegments().get(1); 1478 String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME); 1479 Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME); 1480 if (field == null || add == null) { 1481 throw new IllegalArgumentException("No field/add specified " + uri); 1482 } 1483 ContentValues actualValues = new ContentValues(); 1484 if (cache != null) { 1485 cache.lock(id); 1486 } 1487 try { 1488 db.beginTransaction(); 1489 try { 1490 Cursor c = db.query(tableName, 1491 new String[] {EmailContent.RECORD_ID, field}, 1492 whereWithId(id, selection), 1493 selectionArgs, null, null, null); 1494 try { 1495 result = 0; 1496 String[] bind = new String[1]; 1497 if (c.moveToNext()) { 1498 bind[0] = c.getString(0); // _id 1499 long value = c.getLong(1) + add; 1500 actualValues.put(field, value); 1501 result = db.update(tableName, actualValues, ID_EQUALS, bind); 1502 } 1503 db.setTransactionSuccessful(); 1504 } finally { 1505 c.close(); 1506 } 1507 } finally { 1508 db.endTransaction(); 1509 } 1510 } finally { 1511 if (cache != null) { 1512 cache.unlock(id, actualValues); 1513 } 1514 } 1515 break; 1516 case SYNCED_MESSAGE_ID: 1517 case UPDATED_MESSAGE_ID: 1518 case MESSAGE_ID: 1519 case BODY_ID: 1520 case ATTACHMENT_ID: 1521 case MAILBOX_ID: 1522 case ACCOUNT_ID: 1523 case HOSTAUTH_ID: 1524 case QUICK_RESPONSE_ID: 1525 case POLICY_ID: 1526 id = uri.getPathSegments().get(1); 1527 if (cache != null) { 1528 cache.lock(id); 1529 } 1530 try { 1531 if (match == SYNCED_MESSAGE_ID) { 1532 // For synced messages, first copy the old message to the updated table 1533 // Note the insert or ignore semantics, guaranteeing that only the first 1534 // update will be reflected in the updated message table; therefore this 1535 // row will always have the "original" data 1536 db.execSQL(UPDATED_MESSAGE_INSERT + id); 1537 } else if (match == MESSAGE_ID) { 1538 db.execSQL(UPDATED_MESSAGE_DELETE + id); 1539 } 1540 result = db.update(tableName, values, whereWithId(id, selection), 1541 selectionArgs); 1542 } catch (SQLiteException e) { 1543 // Null out values (so they aren't cached) and re-throw 1544 values = null; 1545 throw e; 1546 } finally { 1547 if (cache != null) { 1548 cache.unlock(id, values); 1549 } 1550 } 1551 if (match == ATTACHMENT_ID) { 1552 if (values.containsKey(Attachment.FLAGS)) { 1553 int flags = values.getAsInteger(Attachment.FLAGS); 1554 mAttachmentService.attachmentChanged(getContext(), 1555 Integer.parseInt(id), flags); 1556 } 1557 } 1558 break; 1559 case BODY: 1560 case MESSAGE: 1561 case UPDATED_MESSAGE: 1562 case ATTACHMENT: 1563 case MAILBOX: 1564 case ACCOUNT: 1565 case HOSTAUTH: 1566 case POLICY: 1567 switch(match) { 1568 // To avoid invalidating the cache on updates, we execute them one at a 1569 // time using the XXX_ID uri; these are all executed atomically 1570 case ACCOUNT: 1571 case MAILBOX: 1572 case HOSTAUTH: 1573 case POLICY: 1574 Cursor c = db.query(tableName, EmailContent.ID_PROJECTION, 1575 selection, selectionArgs, null, null, null); 1576 db.beginTransaction(); 1577 result = 0; 1578 try { 1579 while (c.moveToNext()) { 1580 update(ContentUris.withAppendedId( 1581 uri, c.getLong(EmailContent.ID_PROJECTION_COLUMN)), 1582 values, null, null); 1583 result++; 1584 } 1585 db.setTransactionSuccessful(); 1586 } finally { 1587 db.endTransaction(); 1588 c.close(); 1589 } 1590 break outer; 1591 // Any cached table other than those above should be invalidated here 1592 case MESSAGE: 1593 // If we're doing some generic update, the whole cache needs to be 1594 // invalidated. This case should be quite rare 1595 cache.invalidate("Update", uri, selection); 1596 //$FALL-THROUGH$ 1597 default: 1598 result = db.update(tableName, values, selection, selectionArgs); 1599 break outer; 1600 } 1601 case ACCOUNT_RESET_NEW_COUNT_ID: 1602 id = uri.getPathSegments().get(1); 1603 if (cache != null) { 1604 cache.lock(id); 1605 } 1606 ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 1607 if (values != null) { 1608 Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME); 1609 if (set != null) { 1610 newMessageCount = new ContentValues(); 1611 newMessageCount.put(Account.NEW_MESSAGE_COUNT, set); 1612 } 1613 } 1614 try { 1615 result = db.update(tableName, newMessageCount, 1616 whereWithId(id, selection), selectionArgs); 1617 } finally { 1618 if (cache != null) { 1619 cache.unlock(id, values); 1620 } 1621 } 1622 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1623 break; 1624 case ACCOUNT_RESET_NEW_COUNT: 1625 result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT, 1626 selection, selectionArgs); 1627 // Affects all accounts. Just invalidate all account cache. 1628 cache.invalidate("Reset all new counts", null, null); 1629 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1630 break; 1631 default: 1632 throw new IllegalArgumentException("Unknown URI " + uri); 1633 } 1634 } catch (SQLiteException e) { 1635 checkDatabases(); 1636 throw e; 1637 } 1638 1639 // Notify all notifier cursors 1640 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id); 1641 1642 resolver.notifyChange(notificationUri, null); 1643 return result; 1644 } 1645 1646 /** 1647 * Returns the base notification URI for the given content type. 1648 * 1649 * @param match The type of content that was modified. 1650 */ 1651 private Uri getBaseNotificationUri(int match) { 1652 Uri baseUri = null; 1653 switch (match) { 1654 case MESSAGE: 1655 case MESSAGE_ID: 1656 case SYNCED_MESSAGE_ID: 1657 baseUri = Message.NOTIFIER_URI; 1658 break; 1659 case ACCOUNT: 1660 case ACCOUNT_ID: 1661 baseUri = Account.NOTIFIER_URI; 1662 break; 1663 } 1664 return baseUri; 1665 } 1666 1667 /** 1668 * Sends a change notification to any cursors observers of the given base URI. The final 1669 * notification URI is dynamically built to contain the specified information. It will be 1670 * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending 1671 * upon the given values. 1672 * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked. 1673 * If this is necessary, it can be added. However, due to the implementation of 1674 * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications. 1675 * 1676 * @param baseUri The base URI to send notifications to. Must be able to take appended IDs. 1677 * @param op Optional operation to be appended to the URI. 1678 * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be 1679 * appended to the base URI. 1680 */ 1681 private void sendNotifierChange(Uri baseUri, String op, String id) { 1682 if (baseUri == null) return; 1683 1684 final ContentResolver resolver = getContext().getContentResolver(); 1685 1686 // Append the operation, if specified 1687 if (op != null) { 1688 baseUri = baseUri.buildUpon().appendEncodedPath(op).build(); 1689 } 1690 1691 long longId = 0L; 1692 try { 1693 longId = Long.valueOf(id); 1694 } catch (NumberFormatException ignore) {} 1695 if (longId > 0) { 1696 resolver.notifyChange(ContentUris.withAppendedId(baseUri, longId), null); 1697 } else { 1698 resolver.notifyChange(baseUri, null); 1699 } 1700 1701 // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI. 1702 if (baseUri.equals(Message.NOTIFIER_URI)) { 1703 sendMessageListDataChangedNotification(); 1704 } 1705 } 1706 1707 private void sendMessageListDataChangedNotification() { 1708 final Context context = getContext(); 1709 final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED); 1710 // Ideally this intent would contain information about which account changed, to limit the 1711 // updates to that particular account. Unfortunately, that information is not available in 1712 // sendNotifierChange(). 1713 context.sendBroadcast(intent); 1714 } 1715 1716 @Override 1717 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1718 throws OperationApplicationException { 1719 Context context = getContext(); 1720 SQLiteDatabase db = getDatabase(context); 1721 db.beginTransaction(); 1722 try { 1723 ContentProviderResult[] results = super.applyBatch(operations); 1724 db.setTransactionSuccessful(); 1725 return results; 1726 } finally { 1727 db.endTransaction(); 1728 } 1729 } 1730 1731 /** 1732 * For testing purposes, check whether a given row is cached 1733 * @param baseUri the base uri of the EmailContent 1734 * @param id the row id of the EmailContent 1735 * @return whether or not the row is currently cached 1736 */ 1737 @VisibleForTesting 1738 protected boolean isCached(Uri baseUri, long id) { 1739 int match = findMatch(baseUri, "isCached"); 1740 int table = match >> BASE_SHIFT; 1741 ContentCache cache = mContentCaches[table]; 1742 if (cache == null) return false; 1743 Cursor cc = cache.get(Long.toString(id)); 1744 return (cc != null); 1745 } 1746 1747 public static interface AttachmentService { 1748 /** 1749 * Notify the service that an attachment has changed. 1750 */ 1751 void attachmentChanged(Context context, long id, int flags); 1752 } 1753 1754 private final AttachmentService DEFAULT_ATTACHMENT_SERVICE = new AttachmentService() { 1755 @Override 1756 public void attachmentChanged(Context context, long id, int flags) { 1757 // The default implementation delegates to the real service. 1758 AttachmentDownloadService.attachmentChanged(context, id, flags); 1759 } 1760 }; 1761 private AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE; 1762 1763 /** 1764 * Injects a custom attachment service handler. If null is specified, will reset to the 1765 * default service. 1766 */ 1767 public void injectAttachmentService(AttachmentService as) { 1768 mAttachmentService = (as == null) ? DEFAULT_ATTACHMENT_SERVICE : as; 1769 } 1770 1771 /** 1772 * Support for UnifiedEmail below 1773 */ 1774 1775 private static final String NOT_A_DRAFT_STRING = 1776 Integer.toString(UIProvider.DraftType.NOT_A_DRAFT); 1777 1778 /** 1779 * Mapping of UIProvider columns to EmailProvider columns for the message list (called the 1780 * conversation list in UnifiedEmail) 1781 */ 1782 private static final ProjectionMap sMessageListMap = ProjectionMap.builder() 1783 .add(BaseColumns._ID, MessageColumns.ID) 1784 .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage")) 1785 .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage")) 1786 .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT) 1787 .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET) 1788 .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST) 1789 .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) 1790 .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) 1791 .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1") 1792 .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0") 1793 .add(UIProvider.ConversationColumns.SENDING_STATE, 1794 Integer.toString(ConversationSendingState.OTHER)) 1795 .add(UIProvider.ConversationColumns.PRIORITY, Integer.toString(ConversationPriority.LOW)) 1796 .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ) 1797 .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE) 1798 .add(UIProvider.ConversationColumns.FOLDER_LIST, MessageColumns.MAILBOX_KEY) 1799 .build(); 1800 1801 /** 1802 * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in 1803 * UnifiedEmail 1804 */ 1805 private static final ProjectionMap sMessageViewMap = ProjectionMap.builder() 1806 .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID) 1807 .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID) 1808 .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME)) 1809 .add(UIProvider.MessageColumns.CONVERSATION_ID, 1810 uriWithFQId("uimessage", Message.TABLE_NAME)) 1811 .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT) 1812 .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET) 1813 .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST) 1814 .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST) 1815 .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST) 1816 .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST) 1817 .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST) 1818 .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, EmailContent.MessageColumns.TIMESTAMP) 1819 .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT) 1820 .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT) 1821 .add(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, "0") 1822 .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0") 1823 .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING) 1824 .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0") 1825 .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, EmailContent.MessageColumns.FLAG_ATTACHMENT) 1826 .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, 1827 uriWithFQId("uiattachments", Message.TABLE_NAME)) 1828 .add(UIProvider.MessageColumns.MESSAGE_FLAGS, "0") 1829 .add(UIProvider.MessageColumns.SAVE_MESSAGE_URI, 1830 uriWithFQId("uiupdatedraft", Message.TABLE_NAME)) 1831 .add(UIProvider.MessageColumns.SEND_MESSAGE_URI, 1832 uriWithFQId("uisenddraft", Message.TABLE_NAME)) 1833 .build(); 1834 1835 /** 1836 * Mapping of UIProvider columns to EmailProvider columns for the folder list in UnifiedEmail 1837 */ 1838 private static String getFolderCapabilities() { 1839 return "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL + 1840 ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES + 1841 " ELSE 0 END"; 1842 } 1843 1844 private static final ProjectionMap sFolderListMap = ProjectionMap.builder() 1845 .add(BaseColumns._ID, MessageColumns.ID) 1846 .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) 1847 .add(UIProvider.FolderColumns.NAME, "displayName") 1848 .add(UIProvider.FolderColumns.HAS_CHILDREN, 1849 MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN) 1850 .add(UIProvider.FolderColumns.CAPABILITIES, getFolderCapabilities()) 1851 .add(UIProvider.FolderColumns.SYNC_FREQUENCY, "0") 1852 .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") 1853 .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) 1854 .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders")) 1855 .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT) 1856 .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.MESSAGE_COUNT) 1857 .build(); 1858 1859 /** 1860 * The "ORDER BY" clause for top level folders 1861 */ 1862 private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE 1863 + " WHEN " + Mailbox.TYPE_INBOX + " THEN 0" 1864 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN 1" 1865 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN 2" 1866 + " WHEN " + Mailbox.TYPE_SENT + " THEN 3" 1867 + " WHEN " + Mailbox.TYPE_TRASH + " THEN 4" 1868 + " WHEN " + Mailbox.TYPE_JUNK + " THEN 5" 1869 // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order. 1870 + " ELSE 10 END" 1871 + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 1872 1873 /** 1874 * Generate the SELECT clause using a specified mapping and the original UI projection 1875 * @param map the ProjectionMap to use for this projection 1876 * @param projection the projection as sent by UnifiedEmail 1877 * @return a StringBuilder containing the SELECT expression for a SQLite query 1878 */ 1879 private StringBuilder genSelect(ProjectionMap map, String[] projection) { 1880 StringBuilder sb = new StringBuilder("SELECT "); 1881 boolean first = true; 1882 for (String column: projection) { 1883 if (first) { 1884 first = false; 1885 } else { 1886 sb.append(','); 1887 } 1888 String val = map.get(column); 1889 // If we don't have the column, be permissive, returning "0 AS <column>", and warn 1890 if (val == null) { 1891 Log.w(TAG, "UIProvider column not found, returning 0: " + column); 1892 val = "0 AS " + column; 1893 } 1894 sb.append(val); 1895 } 1896 return sb; 1897 } 1898 1899 /** 1900 * Convenience method to create a Uri string given the "type" of query; we append the type 1901 * of the query and the id column name (_id) 1902 * 1903 * @param type the "type" of the query, as defined by our UriMatcher definitions 1904 * @return a Uri string 1905 */ 1906 private static String uriWithId(String type) { 1907 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || _id"; 1908 } 1909 1910 /** 1911 * Convenience method to create a Uri string given the "type" of query and the table name to 1912 * which it applies; we append the type of the query and the fully qualified (FQ) id column 1913 * (i.e. including the table name); we need this for join queries where _id would otherwise 1914 * be ambiguous 1915 * 1916 * @param type the "type" of the query, as defined by our UriMatcher definitions 1917 * @param tableName the name of the table whose _id is referred to 1918 * @return a Uri string 1919 */ 1920 private static String uriWithFQId(String type, String tableName) { 1921 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id"; 1922 } 1923 1924 /** 1925 * Generate the "view message" SQLite query, given a projection from UnifiedEmail 1926 * 1927 * @param uiProjection as passed from UnifiedEmail 1928 * @return the SQLite query to be executed on the EmailProvider database 1929 */ 1930 private String genQueryViewMessage(String[] uiProjection) { 1931 StringBuilder sb = genSelect(sMessageViewMap, uiProjection); 1932 sb.append(" FROM " + Message.TABLE_NAME + "," + Body.TABLE_NAME + " WHERE " + 1933 Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " AND " + 1934 Message.TABLE_NAME + "." + Message.RECORD_ID + "=?"); 1935 return sb.toString(); 1936 } 1937 1938 /** 1939 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 1940 * 1941 * @param uiProjection as passed from UnifiedEmail 1942 * @return the SQLite query to be executed on the EmailProvider database 1943 */ 1944 private String genQueryMailboxMessages(String[] uiProjection) { 1945 StringBuilder sb = genSelect(sMessageListMap, uiProjection); 1946 // Make constant 1947 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.MAILBOX_KEY + "=? ORDER BY " + 1948 MessageColumns.TIMESTAMP + " DESC"); 1949 return sb.toString(); 1950 } 1951 1952 /** 1953 * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail 1954 * 1955 * @param uiProjection as passed from UnifiedEmail 1956 * @return the SQLite query to be executed on the EmailProvider database 1957 */ 1958 private String genQueryAccountMailboxes(String[] uiProjection) { 1959 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 1960 // Make constant 1961 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 1962 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 1963 " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY "); 1964 sb.append(MAILBOX_ORDER_BY); 1965 return sb.toString(); 1966 } 1967 1968 /** 1969 * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail 1970 * 1971 * @param uiProjection as passed from UnifiedEmail 1972 * @return the SQLite query to be executed on the EmailProvider database 1973 */ 1974 private String genQuerySubfolders(String[] uiProjection) { 1975 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 1976 // Make constant 1977 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY + 1978 " =? ORDER BY "); 1979 sb.append(MAILBOX_ORDER_BY); 1980 return sb.toString(); 1981 } 1982 1983 /** 1984 * Given the email address of an account, return its account id (the _id row in the Account 1985 * table), or NO_ACCOUNT (-1) if not found 1986 * 1987 * @param email the email address of the account 1988 * @return the account id for this account, or NO_ACCOUNT if not found 1989 */ 1990 private long findAccountIdByName(String email) { 1991 Map<String, Cursor> accountCache = mCacheAccount.getSnapshot(); 1992 Collection<Cursor> accounts = accountCache.values(); 1993 for (Cursor accountCursor: accounts) { 1994 if (accountCursor.getString(Account.CONTENT_EMAIL_ADDRESS_COLUMN).equals(email)) { 1995 return accountCursor.getLong(Account.CONTENT_ID_COLUMN); 1996 } 1997 } 1998 return Account.NO_ACCOUNT; 1999 } 2000 2001 /** 2002 * Handle UnifiedEmail queries here (dispatched from query()) 2003 * 2004 * @param match the UriMatcher match for the original uri passed in from UnifiedEmail 2005 * @param uri the original uri passed in from UnifiedEmail 2006 * @param uiProjection the projection passed in from UnifiedEmail 2007 * @return the result Cursor 2008 */ 2009 private Cursor uiQuery(int match, Uri uri, String[] uiProjection) { 2010 Context context = getContext(); 2011 SQLiteDatabase db = getDatabase(context); 2012 // Should we ever return null, or throw an exception?? 2013 Cursor c = null; 2014 String id = uri.getPathSegments().get(1); 2015 switch(match) { 2016 case UI_FOLDERS: 2017 // We are passed the email address (unique account identifier) in the uri; we 2018 // need to turn this into the _id of the Account row in the EmailProvider db 2019 String accountName = id; 2020 long acctId = findAccountIdByName(accountName); 2021 if (acctId == Account.NO_ACCOUNT) return null; 2022 c = db.rawQuery(genQueryAccountMailboxes(uiProjection), 2023 new String[] {Long.toString(acctId)}); 2024 break; 2025 case UI_SUBFOLDERS: 2026 c = db.rawQuery(genQuerySubfolders(uiProjection), new String[] {id}); 2027 break; 2028 case UI_MESSAGES: 2029 c = db.rawQuery(genQueryMailboxMessages(uiProjection), new String[] {id}); 2030 break; 2031 case UI_MESSAGE: 2032 c = db.rawQuery(genQueryViewMessage(uiProjection), new String[] {id}); 2033 break; 2034 } 2035 if (c != null) { 2036 // Notify UIProvider on changes 2037 // Make this more specific to actual query later on... 2038 c.setNotificationUri(context.getContentResolver(), UIPROVIDER_MESSAGE_NOTIFIER); 2039 } 2040 return c; 2041 } 2042 2043 /** 2044 * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need 2045 * a few of the fields 2046 * @param uiAtt the UIProvider attachment to convert 2047 * @return the EmailProvider attachment 2048 */ 2049 private Attachment convertUiAttachmentToAttachment( 2050 com.android.mail.providers.Attachment uiAtt) { 2051 Attachment att = new Attachment(); 2052 att.mContentUri = uiAtt.contentUri; 2053 att.mFileName = uiAtt.name; 2054 att.mMimeType = uiAtt.mimeType; 2055 att.mSize = uiAtt.size; 2056 return att; 2057 } 2058 2059 /** 2060 * Create a mailbox given the account and mailboxType. 2061 */ 2062 private Mailbox createMailbox(long accountId, int mailboxType) { 2063 Context context = getContext(); 2064 int resId = -1; 2065 switch (mailboxType) { 2066 case Mailbox.TYPE_INBOX: 2067 resId = R.string.mailbox_name_server_inbox; 2068 break; 2069 case Mailbox.TYPE_OUTBOX: 2070 resId = R.string.mailbox_name_server_outbox; 2071 break; 2072 case Mailbox.TYPE_DRAFTS: 2073 resId = R.string.mailbox_name_server_drafts; 2074 break; 2075 case Mailbox.TYPE_TRASH: 2076 resId = R.string.mailbox_name_server_trash; 2077 break; 2078 case Mailbox.TYPE_SENT: 2079 resId = R.string.mailbox_name_server_sent; 2080 break; 2081 case Mailbox.TYPE_JUNK: 2082 resId = R.string.mailbox_name_server_junk; 2083 break; 2084 default: 2085 throw new IllegalArgumentException("Illegal mailbox type"); 2086 } 2087 Log.d(TAG, "Creating mailbox of type " + mailboxType + " for account " + accountId); 2088 Mailbox box = Mailbox.newSystemMailbox(accountId, mailboxType, context.getString(resId)); 2089 box.save(context); 2090 return box; 2091 } 2092 2093 /** 2094 * Given an account name and a mailbox type, return that mailbox, creating it if necessary 2095 * @param accountName the account name to use 2096 * @param mailboxType the type of mailbox we're trying to find 2097 * @return the mailbox of the given type for the account in the uri, or null if not found 2098 */ 2099 private Mailbox getMailboxByUriAndType(String accountName, int mailboxType) { 2100 long accountId = findAccountIdByName(accountName); 2101 if (accountId == Account.NO_ACCOUNT) return null; 2102 Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), accountId, mailboxType); 2103 if (mailbox == null) { 2104 mailbox = createMailbox(accountId, mailboxType); 2105 } 2106 return mailbox; 2107 } 2108 2109 private Message getMessageFromPathSegments(List<String> pathSegments) { 2110 Message msg = null; 2111 if (pathSegments.size() > 2) { 2112 msg = Message.restoreMessageWithId(getContext(), Long.parseLong(pathSegments.get(2))); 2113 } 2114 if (msg == null) { 2115 msg = new Message(); 2116 } 2117 return msg; 2118 } 2119 /** 2120 * Given a mailbox and the content values for a message, create/save the message in the mailbox 2121 * @param mailbox the mailbox to use 2122 * @param values the content values that represent message fields 2123 * @return the uri of the newly created message 2124 */ 2125 private Uri uiSaveMessage(Message msg, Mailbox mailbox, ContentValues values) { 2126 Context context = getContext(); 2127 // Fill in the message 2128 msg.mTo = values.getAsString(UIProvider.MessageColumns.TO); 2129 msg.mCc = values.getAsString(UIProvider.MessageColumns.CC); 2130 msg.mBcc = values.getAsString(UIProvider.MessageColumns.BCC); 2131 msg.mSubject = values.getAsString(UIProvider.MessageColumns.SUBJECT); 2132 msg.mText = values.getAsString(UIProvider.MessageColumns.BODY_TEXT); 2133 msg.mHtml = values.getAsString(UIProvider.MessageColumns.BODY_HTML); 2134 msg.mMailboxKey = mailbox.mId; 2135 msg.mAccountKey = mailbox.mAccountKey; 2136 msg.mDisplayName = msg.mTo; 2137 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 2138 // Get attachments from the ContentValues 2139 ArrayList<com.android.mail.providers.Attachment> uiAtts = 2140 com.android.mail.providers.Attachment.getAttachmentsFromJoinedAttachmentInfo( 2141 values.getAsString(UIProvider.MessageColumns.JOINED_ATTACHMENT_INFOS)); 2142 ArrayList<Attachment> atts = new ArrayList<Attachment>(); 2143 for (com.android.mail.providers.Attachment uiAtt: uiAtts) { 2144 // Convert to our attachments and add to the list; everything else should "just work" 2145 atts.add(convertUiAttachmentToAttachment(uiAtt)); 2146 } 2147 if (!atts.isEmpty()) { 2148 msg.mAttachments = atts; 2149 } 2150 // Save it or update it... 2151 if (!msg.isSaved()) { 2152 msg.save(context); 2153 } else { 2154 // This is tricky due to how messages/attachments are saved; rather than putz with 2155 // what's changed, we'll delete/re-add them 2156 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 2157 // Delete all existing attachments 2158 ops.add(ContentProviderOperation.newDelete( 2159 ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId)) 2160 .build()); 2161 // Delete the body 2162 ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI) 2163 .withSelection(Body.MESSAGE_KEY + "=?", new String[] {Long.toString(msg.mId)}) 2164 .build()); 2165 // Add the ops for the message, atts, and body 2166 msg.addSaveOps(ops); 2167 // Do it! 2168 try { 2169 applyBatch(ops); 2170 } catch (OperationApplicationException e) { 2171 } 2172 } 2173 return Uri.parse("content://" + EmailContent.AUTHORITY + "/uimessage/" + msg.mId); 2174 } 2175 2176 /** 2177 * Create and send the message via the account indicated in the uri 2178 * @param uri the incoming uri 2179 * @param values the content values that represent message fields 2180 * @return the uri of the created message 2181 */ 2182 private Uri uiSendMail(Uri uri, ContentValues values) { 2183 List<String> pathSegments = uri.getPathSegments(); 2184 Mailbox mailbox = getMailboxByUriAndType(pathSegments.get(1), Mailbox.TYPE_OUTBOX); 2185 if (mailbox == null) return null; 2186 Message msg = getMessageFromPathSegments(pathSegments); 2187 try { 2188 return uiSaveMessage(msg, mailbox, values); 2189 } finally { 2190 // Kick observers 2191 getContext().getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 2192 } 2193 } 2194 2195 /** 2196 * Create a message and save it to the drafts folder of the account indicated in the uri 2197 * @param uri the incoming uri 2198 * @param values the content values that represent message fields 2199 * @return the uri of the created message 2200 */ 2201 private Uri uiSaveDraft(Uri uri, ContentValues values) { 2202 List<String> pathSegments = uri.getPathSegments(); 2203 Mailbox mailbox = getMailboxByUriAndType(pathSegments.get(1), Mailbox.TYPE_DRAFTS); 2204 if (mailbox == null) return null; 2205 Message msg = getMessageFromPathSegments(pathSegments); 2206 return uiSaveMessage(msg, mailbox, values); 2207 } 2208 2209 private int uiUpdateDraft(Uri uri, ContentValues values) { 2210 Context context = getContext(); 2211 Message msg = Message.restoreMessageWithId(context, 2212 Long.parseLong(uri.getPathSegments().get(1))); 2213 if (msg == null) return 0; 2214 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 2215 if (mailbox == null) return 0; 2216 uiSaveMessage(msg, mailbox, values); 2217 return 1; 2218 } 2219 2220 private int uiSendDraft(Uri uri, ContentValues values) { 2221 Context context = getContext(); 2222 Message msg = Message.restoreMessageWithId(context, 2223 Long.parseLong(uri.getPathSegments().get(1))); 2224 if (msg == null) return 0; 2225 long mailboxId = Mailbox.findMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_OUTBOX); 2226 if (mailboxId == Mailbox.NO_MAILBOX) return 0; 2227 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 2228 if (mailbox == null) return 0; 2229 uiSaveMessage(msg, mailbox, values); 2230 // Kick observers 2231 context.getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 2232 return 1; 2233 } 2234 2235 private void putIntegerLongOrBoolean(ContentValues values, String columnName, Object value) { 2236 if (value instanceof Integer) { 2237 Integer intValue = (Integer)value; 2238 values.put(columnName, intValue); 2239 } else if (value instanceof Boolean) { 2240 Boolean boolValue = (Boolean)value; 2241 values.put(columnName, boolValue ? 1 : 0); 2242 } else if (value instanceof Long) { 2243 Long longValue = (Long)value; 2244 values.put(columnName, longValue); 2245 } 2246 } 2247 2248 private ContentValues convertUiMessageValues(ContentValues values) { 2249 ContentValues ourValues = new ContentValues(); 2250 for (String columnName: values.keySet()) { 2251 Object val = values.get(columnName); 2252 if (columnName.equals(UIProvider.ConversationColumns.STARRED)) { 2253 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val); 2254 } else if (columnName.equals(UIProvider.ConversationColumns.READ)) { 2255 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val); 2256 } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 2257 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val); 2258 } else if (columnName.equals(UIProvider.ConversationColumns.FOLDER_LIST)) { 2259 // Convert from folder list uri to mailbox key 2260 Uri uri = Uri.parse((String)val); 2261 Long mailboxId = Long.parseLong(uri.getLastPathSegment()); 2262 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId); 2263 } else { 2264 throw new IllegalArgumentException("Can't update " + columnName + " in message"); 2265 } 2266 } 2267 return ourValues; 2268 } 2269 2270 private Uri convertToEmailProviderUri(Uri uri, boolean asProvider) { 2271 String idString = uri.getLastPathSegment(); 2272 try { 2273 long id = Long.parseLong(idString); 2274 Uri ourUri = ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id); 2275 if (asProvider) { 2276 ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build(); 2277 } 2278 return ourUri; 2279 } catch (NumberFormatException e) { 2280 return null; 2281 } 2282 } 2283 2284 private Message getMessageFromLastSegment(Uri uri) { 2285 long messageId = Long.parseLong(uri.getLastPathSegment()); 2286 return Message.restoreMessageWithId(getContext(), messageId); 2287 } 2288 2289 /** 2290 * Add an undo operation for the current sequence; if the sequence is newer than what we've had, 2291 * clear out the undo list and start over 2292 * @param uri the uri we're working on 2293 * @param op the ContentProviderOperation to perform upon undo 2294 */ 2295 private void addToSequence(Uri uri, ContentProviderOperation op) { 2296 String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER); 2297 if (sequenceString != null) { 2298 int sequence = Integer.parseInt(sequenceString); 2299 if (sequence > mLastSequence) { 2300 // Reset sequence 2301 mLastSequenceOps.clear(); 2302 mLastSequence = sequence; 2303 } 2304 // TODO: Need something to indicate a change isn't ready (undoable) 2305 mLastSequenceOps.add(op); 2306 } 2307 } 2308 2309 private int uiUpdateMessage(Uri uri, ContentValues values) { 2310 Uri ourUri = convertToEmailProviderUri(uri, true); 2311 if (ourUri == null) return 0; 2312 ContentValues ourValues = convertUiMessageValues(values); 2313 Message msg = getMessageFromLastSegment(uri); 2314 if (msg == null) return 0; 2315 ContentValues undoValues = new ContentValues(); 2316 for (String columnName: ourValues.keySet()) { 2317 if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 2318 undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey); 2319 } else if (columnName.equals(MessageColumns.FLAG_READ)) { 2320 undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead); 2321 } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) { 2322 undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite); 2323 } 2324 } 2325 ContentProviderOperation op = 2326 ContentProviderOperation.newUpdate(convertToEmailProviderUri(uri, false)) 2327 .withValues(undoValues) 2328 .build(); 2329 addToSequence(uri, op); 2330 return update(ourUri, ourValues, null, null); 2331 } 2332 2333 private int uiDeleteMessage(Uri uri) { 2334 Context context = getContext(); 2335 Message msg = getMessageFromLastSegment(uri); 2336 if (msg == null) return 0; 2337 Mailbox mailbox = 2338 Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH); 2339 if (mailbox == null) return 0; 2340 ContentProviderOperation op = 2341 ContentProviderOperation.newUpdate(convertToEmailProviderUri(uri, false)) 2342 .withValue(Message.MAILBOX_KEY, msg.mMailboxKey) 2343 .build(); 2344 addToSequence(uri, op); 2345 ContentValues values = new ContentValues(); 2346 values.put(Message.MAILBOX_KEY, mailbox.mId); 2347 return uiUpdateMessage(uri, values); 2348 } 2349 2350 private Cursor uiUndo(Uri uri, String[] projection) { 2351 // First see if we have any operations saved 2352 // TODO: Make sure seq matches 2353 if (!mLastSequenceOps.isEmpty()) { 2354 try { 2355 // TODO Always use this projection? Or what's passed in? 2356 // Not sure if UI wants it, but I'm making a cursor of convo uri's 2357 MatrixCursor c = new MatrixCursor( 2358 new String[] {UIProvider.ConversationColumns.URI}, 2359 mLastSequenceOps.size()); 2360 for (ContentProviderOperation op: mLastSequenceOps) { 2361 c.addRow(new String[] {op.getUri().toString()}); 2362 } 2363 // Just apply the batch and we're done! 2364 applyBatch(mLastSequenceOps); 2365 // But clear the operations 2366 mLastSequenceOps.clear(); 2367 // Tell the UI there are changes 2368 notifyUIProvider("Undo"); 2369 return c; 2370 } catch (OperationApplicationException e) { 2371 } 2372 } 2373 return new MatrixCursor(projection, 0); 2374 } 2375 2376 private void notifyUIProvider(String reason) { 2377 getContext().getContentResolver().notifyChange(UIPROVIDER_MESSAGE_NOTIFIER, null); 2378 // Temporary 2379 Log.d(TAG, "[Notify UIProvider " + reason + "]"); 2380 } 2381} 2382