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