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