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