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