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