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