EmailProvider.java revision 4bf4edcf64c0b1cb8aabfb2e16ec29f4b19921d2
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, 1941 "'content://" + EmailContent.AUTHORITY + "/uifolder/' || " 1942 + MessageColumns.MAILBOX_KEY) 1943 .build(); 1944 1945 /** 1946 * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in 1947 * UnifiedEmail 1948 */ 1949 private static final ProjectionMap sMessageViewMap = ProjectionMap.builder() 1950 .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID) 1951 .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID) 1952 .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME)) 1953 .add(UIProvider.MessageColumns.CONVERSATION_ID, 1954 uriWithFQId("uimessage", Message.TABLE_NAME)) 1955 .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT) 1956 .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET) 1957 .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST) 1958 .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST) 1959 .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST) 1960 .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST) 1961 .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST) 1962 .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, EmailContent.MessageColumns.TIMESTAMP) 1963 .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT) 1964 .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT) 1965 .add(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, "0") 1966 .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0") 1967 .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING) 1968 .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0") 1969 .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, EmailContent.MessageColumns.FLAG_ATTACHMENT) 1970 .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, 1971 uriWithFQId("uiattachments", Message.TABLE_NAME)) 1972 .add(UIProvider.MessageColumns.MESSAGE_FLAGS, "0") 1973 .add(UIProvider.MessageColumns.SAVE_MESSAGE_URI, 1974 uriWithFQId("uiupdatedraft", Message.TABLE_NAME)) 1975 .add(UIProvider.MessageColumns.SEND_MESSAGE_URI, 1976 uriWithFQId("uisenddraft", Message.TABLE_NAME)) 1977 // TODO(pwestbro): make this actually return valid results. 1978 .add(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, "0") 1979 .build(); 1980 1981 /** 1982 * Mapping of UIProvider columns to EmailProvider columns for the folder list in UnifiedEmail 1983 */ 1984 private static String getFolderCapabilities() { 1985 return "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL + 1986 ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES + 1987 " ELSE 0 END"; 1988 } 1989 1990 private static final ProjectionMap sFolderListMap = ProjectionMap.builder() 1991 .add(BaseColumns._ID, MailboxColumns.ID) 1992 .add(UIProvider.FolderColumns.ID, MailboxColumns.ID) 1993 .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) 1994 .add(UIProvider.FolderColumns.NAME, "displayName") 1995 .add(UIProvider.FolderColumns.HAS_CHILDREN, 1996 MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN) 1997 .add(UIProvider.FolderColumns.CAPABILITIES, getFolderCapabilities()) 1998 .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") 1999 .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) 2000 .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders")) 2001 .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT) 2002 .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.MESSAGE_COUNT) 2003 .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId("uirefresh")) 2004 .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS) 2005 .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT) 2006 .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.TOTAL_COUNT) 2007 .build(); 2008 2009 private static final ProjectionMap sAccountListMap = ProjectionMap.builder() 2010 .add(BaseColumns._ID, AccountColumns.ID) 2011 .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders")) 2012 .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME) 2013 .add(UIProvider.AccountColumns.SAVE_DRAFT_URI, uriWithId("uisavedraft")) 2014 .add(UIProvider.AccountColumns.SEND_MAIL_URI, uriWithId("uisendmail")) 2015 .add(UIProvider.AccountColumns.UNDO_URI, uriWithId("uiundo")) 2016 .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount")) 2017 .add(UIProvider.AccountColumns.SETTINGS_QUERY_URI, uriWithId("uisettings")) 2018 .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch")) 2019 // TODO: Is this used? 2020 .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1") 2021 .add(UIProvider.AccountColumns.SYNC_STATUS, "0") 2022 .build(); 2023 2024 /** 2025 * The "ORDER BY" clause for top level folders 2026 */ 2027 private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE 2028 + " WHEN " + Mailbox.TYPE_INBOX + " THEN 0" 2029 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN 1" 2030 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN 2" 2031 + " WHEN " + Mailbox.TYPE_SENT + " THEN 3" 2032 + " WHEN " + Mailbox.TYPE_TRASH + " THEN 4" 2033 + " WHEN " + Mailbox.TYPE_JUNK + " THEN 5" 2034 // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order. 2035 + " ELSE 10 END" 2036 + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 2037 2038 2039 /** 2040 * Mapping of UIProvider columns to EmailProvider columns for the message list (called the 2041 * conversation list in UnifiedEmail) 2042 */ 2043 private static final ProjectionMap sAccountSettingsMap = ProjectionMap.builder() 2044 .add(UIProvider.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE) 2045 .add(UIProvider.SettingsColumns.AUTO_ADVANCE, 2046 Integer.toString(UIProvider.AutoAdvance.NEWER)) 2047 .add(UIProvider.SettingsColumns.MESSAGE_TEXT_SIZE, 2048 Integer.toString(UIProvider.MessageTextSize.NORMAL)) 2049 .add(UIProvider.SettingsColumns.SNAP_HEADERS, 2050 Integer.toString(UIProvider.SnapHeaderValue.ALWAYS)) 2051 .add(UIProvider.SettingsColumns.REPLY_BEHAVIOR, 2052 Integer.toString(UIProvider.DefaultReplyBehavior.REPLY)) 2053 .add(UIProvider.SettingsColumns.HIDE_CHECKBOXES, "0") 2054 .add(UIProvider.SettingsColumns.CONFIRM_DELETE, "0") 2055 .add(UIProvider.SettingsColumns.CONFIRM_ARCHIVE, "0") 2056 .add(UIProvider.SettingsColumns.CONFIRM_SEND, "0") 2057 .build(); 2058 2059 /** 2060 * Mapping of UIProvider columns to EmailProvider columns for a message's attachments 2061 */ 2062 private static final ProjectionMap sAttachmentMap = ProjectionMap.builder() 2063 .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME) 2064 .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE) 2065 .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment")) 2066 .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE) 2067 .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE) 2068 .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION) 2069 .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE, AttachmentColumns.UI_DOWNLOADED_SIZE) 2070 .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI) 2071 .build(); 2072 2073 /** 2074 * Generate the SELECT clause using a specified mapping and the original UI projection 2075 * @param map the ProjectionMap to use for this projection 2076 * @param projection the projection as sent by UnifiedEmail 2077 * @param values ContentValues to be used if the ProjectionMap entry is null 2078 * @return a StringBuilder containing the SELECT expression for a SQLite query 2079 */ 2080 private StringBuilder genSelect(ProjectionMap map, String[] projection) { 2081 return genSelect(map, projection, EMPTY_CONTENT_VALUES); 2082 } 2083 2084 private StringBuilder genSelect(ProjectionMap map, String[] projection, ContentValues values) { 2085 StringBuilder sb = new StringBuilder("SELECT "); 2086 boolean first = true; 2087 for (String column: projection) { 2088 if (first) { 2089 first = false; 2090 } else { 2091 sb.append(','); 2092 } 2093 String val = null; 2094 // First look at values; this is an override of default behavior 2095 if (values.containsKey(column)) { 2096 val = "'" + values.getAsString(column) + "' AS " + column; 2097 } else { 2098 // Now, get the standard value for the column from our projection map 2099 val = map.get(column); 2100 // If we don't have the column, return "NULL AS <column>", and warn 2101 if (val == null) { 2102 Log.w(TAG, "UIProvider column not found, returning NULL: " + column); 2103 val = "NULL AS " + column; 2104 } 2105 } 2106 sb.append(val); 2107 } 2108 return sb; 2109 } 2110 2111 /** 2112 * Convenience method to create a Uri string given the "type" of query; we append the type 2113 * of the query and the id column name (_id) 2114 * 2115 * @param type the "type" of the query, as defined by our UriMatcher definitions 2116 * @return a Uri string 2117 */ 2118 private static String uriWithId(String type) { 2119 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || _id"; 2120 } 2121 2122 /** 2123 * Convenience method to create a Uri string given the "type" of query and the table name to 2124 * which it applies; we append the type of the query and the fully qualified (FQ) id column 2125 * (i.e. including the table name); we need this for join queries where _id would otherwise 2126 * be ambiguous 2127 * 2128 * @param type the "type" of the query, as defined by our UriMatcher definitions 2129 * @param tableName the name of the table whose _id is referred to 2130 * @return a Uri string 2131 */ 2132 private static String uriWithFQId(String type, String tableName) { 2133 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id"; 2134 } 2135 2136 /** 2137 * Generate the "view message" SQLite query, given a projection from UnifiedEmail 2138 * 2139 * @param uiProjection as passed from UnifiedEmail 2140 * @return the SQLite query to be executed on the EmailProvider database 2141 */ 2142 private String genQueryViewMessage(String[] uiProjection, String id) { 2143 Context context = getContext(); 2144 long messageId = Long.parseLong(id); 2145 Message msg = Message.restoreMessageWithId(context, messageId); 2146 if (msg != null && (msg.mFlagLoaded == Message.FLAG_LOADED_PARTIAL)) { 2147 EmailServiceProxy service = 2148 EmailServiceUtils.getServiceForAccount(context, null, msg.mAccountKey); 2149 try { 2150 service.loadMore(messageId); 2151 } catch (RemoteException e) { 2152 // Nothing to do 2153 } 2154 } 2155 StringBuilder sb = genSelect(sMessageViewMap, uiProjection); 2156 sb.append(" FROM " + Message.TABLE_NAME + "," + Body.TABLE_NAME + " WHERE " + 2157 Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " AND " + 2158 Message.TABLE_NAME + "." + Message.RECORD_ID + "=?"); 2159 return sb.toString(); 2160 } 2161 2162 /** 2163 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 2164 * 2165 * @param uiProjection as passed from UnifiedEmail 2166 * @return the SQLite query to be executed on the EmailProvider database 2167 */ 2168 private String genQueryMailboxMessages(String[] uiProjection) { 2169 StringBuilder sb = genSelect(sMessageListMap, uiProjection); 2170 // Make constant 2171 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.MAILBOX_KEY + "=? ORDER BY " + 2172 MessageColumns.TIMESTAMP + " DESC"); 2173 return sb.toString(); 2174 } 2175 2176 /** 2177 * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail 2178 * 2179 * @param uiProjection as passed from UnifiedEmail 2180 * @return the SQLite query to be executed on the EmailProvider database 2181 */ 2182 private String genQueryAccountMailboxes(String[] uiProjection) { 2183 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 2184 // Make constant 2185 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2186 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2187 " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY "); 2188 sb.append(MAILBOX_ORDER_BY); 2189 return sb.toString(); 2190 } 2191 2192 /** 2193 * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail 2194 * 2195 * @param uiProjection as passed from UnifiedEmail 2196 * @return the SQLite query to be executed on the EmailProvider database 2197 */ 2198 private String genQueryMailbox(String[] uiProjection, String id) { 2199 long mailboxId = Long.parseLong(id); 2200 ContentValues values = EMPTY_CONTENT_VALUES; 2201 if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) { 2202 // This is the current search mailbox; use the total count 2203 values = new ContentValues(); 2204 values.put(UIProvider.FolderColumns.TOTAL_COUNT, mSearchParams.mTotalCount); 2205 // "load more" is valid for search results 2206 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 2207 uiUriString("uiloadmore", mailboxId)); 2208 } else { 2209 Context context = getContext(); 2210 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 2211 String protocol = Account.getProtocol(context, mailbox.mAccountKey); 2212 // "load more" is valid for IMAP/POP3 2213 if (HostAuth.SCHEME_IMAP.equals(protocol) || HostAuth.SCHEME_POP3.equals(protocol)) { 2214 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 2215 uiUriString("uiloadmore", mailboxId)); 2216 } 2217 } 2218 StringBuilder sb = genSelect(sFolderListMap, uiProjection, values); 2219 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ID + "=?"); 2220 return sb.toString(); 2221 } 2222 2223 private static final long IMAP_CAPABILITIES = 2224 AccountCapabilities.SYNCABLE_FOLDERS | 2225 AccountCapabilities.FOLDER_SERVER_SEARCH | 2226 AccountCapabilities.UNDO; 2227 2228 private static final long POP3_CAPABILITIES = 0; 2229 2230 private static final long EAS_12_CAPABILITIES = 2231 AccountCapabilities.SYNCABLE_FOLDERS | 2232 AccountCapabilities.FOLDER_SERVER_SEARCH | 2233 AccountCapabilities.SANITIZED_HTML | 2234 AccountCapabilities.SMART_REPLY | 2235 AccountCapabilities.SERVER_SEARCH | 2236 AccountCapabilities.UNDO; 2237 2238 private static final long EAS_2_CAPABILITIES = 2239 AccountCapabilities.SYNCABLE_FOLDERS | 2240 AccountCapabilities.SANITIZED_HTML | 2241 AccountCapabilities.SMART_REPLY | 2242 AccountCapabilities.UNDO; 2243 2244 private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://ui.email.android.com"); 2245 2246 private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com"); 2247 2248 private static String getExternalUriString(String segment, String account) { 2249 return BASE_EXTERNAL_URI.buildUpon().appendPath(segment) 2250 .appendQueryParameter("account", account).build().toString(); 2251 } 2252 2253 private static String getExternalUriStringEmail2(String segment, String account) { 2254 return BASE_EXTERAL_URI2.buildUpon().appendPath(segment) 2255 .appendQueryParameter("account", account).build().toString(); 2256 } 2257 2258 /** 2259 * Generate a "single account" SQLite query, given a projection from UnifiedEmail 2260 * 2261 * @param uiProjection as passed from UnifiedEmail 2262 * @return the SQLite query to be executed on the EmailProvider database 2263 */ 2264 // TODO: Get protocol specific stuff out of here (it should be in the account) 2265 private String genQueryAccount(String[] uiProjection, String id) { 2266 ContentValues values = new ContentValues(); 2267 long accountId = Long.parseLong(id); 2268 String protocol = Account.getProtocol(getContext(), accountId); 2269 if (HostAuth.SCHEME_IMAP.equals(protocol)) { 2270 values.put(UIProvider.AccountColumns.CAPABILITIES, IMAP_CAPABILITIES); 2271 } else if (HostAuth.SCHEME_POP3.equals(protocol)) { 2272 values.put(UIProvider.AccountColumns.CAPABILITIES, POP3_CAPABILITIES); 2273 } else { 2274 Account account = Account.restoreAccountWithId(getContext(), accountId); 2275 String easVersion = account.mProtocolVersion; 2276 Double easVersionDouble = 2.5D; 2277 if (easVersion != null) { 2278 try { 2279 easVersionDouble = Double.parseDouble(easVersion); 2280 } catch (NumberFormatException e) { 2281 // Stick with 2.5 2282 } 2283 } 2284 if (easVersionDouble >= 12.0D) { 2285 values.put(UIProvider.AccountColumns.CAPABILITIES, EAS_12_CAPABILITIES); 2286 } else { 2287 values.put(UIProvider.AccountColumns.CAPABILITIES, EAS_2_CAPABILITIES); 2288 } 2289 } 2290 values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI, 2291 getExternalUriString("settings", id)); 2292 values.put(UIProvider.AccountColumns.COMPOSE_URI, 2293 getExternalUriStringEmail2("compose", id)); 2294 values.put(UIProvider.AccountColumns.MIME_TYPE, "application/email-ls"); 2295 StringBuilder sb = genSelect(sAccountListMap, uiProjection, values); 2296 sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?"); 2297 return sb.toString(); 2298 } 2299 2300 /** 2301 * Generate an "account settings" SQLite query, given a projection from UnifiedEmail 2302 * 2303 * @param uiProjection as passed from UnifiedEmail 2304 * @return the SQLite query to be executed on the EmailProvider database 2305 */ 2306 private String genQuerySettings(String[] uiProjection, String id) { 2307 ContentValues values = new ContentValues(); 2308 long accountId = Long.parseLong(id); 2309 long mailboxId = Mailbox.findMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX); 2310 if (mailboxId != Mailbox.NO_MAILBOX) { 2311 values.put(UIProvider.SettingsColumns.DEFAULT_INBOX, 2312 uiUriString("uifolder", mailboxId)); 2313 } 2314 StringBuilder sb = genSelect(sAccountSettingsMap, uiProjection, values); 2315 sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?"); 2316 return sb.toString(); 2317 } 2318 2319 private Cursor uiAccounts(String[] uiProjection) { 2320 Context context = getContext(); 2321 SQLiteDatabase db = getDatabase(context); 2322 Cursor accountIdCursor = 2323 db.rawQuery("select _id from " + Account.TABLE_NAME, new String[0]); 2324 MatrixCursor mc = new MatrixCursor(uiProjection, accountIdCursor.getCount()); 2325 Object[] values = new Object[uiProjection.length]; 2326 try { 2327 while (accountIdCursor.moveToNext()) { 2328 String id = accountIdCursor.getString(0); 2329 Cursor accountCursor = 2330 db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); 2331 if (accountCursor.moveToNext()) { 2332 for (int i = 0; i < uiProjection.length; i++) { 2333 values[i] = accountCursor.getString(i); 2334 } 2335 mc.addRow(values); 2336 } 2337 accountCursor.close(); 2338 } 2339 } finally { 2340 accountIdCursor.close(); 2341 } 2342 return mc; 2343 } 2344 2345 /** 2346 * Generate the "attachment list" SQLite query, given a projection from UnifiedEmail 2347 * 2348 * @param uiProjection as passed from UnifiedEmail 2349 * @return the SQLite query to be executed on the EmailProvider database 2350 */ 2351 private String genQueryAttachments(String[] uiProjection) { 2352 StringBuilder sb = genSelect(sAttachmentMap, uiProjection); 2353 sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.MESSAGE_KEY + 2354 " =? "); 2355 return sb.toString(); 2356 } 2357 2358 /** 2359 * Generate the "single attachment" SQLite query, given a projection from UnifiedEmail 2360 * 2361 * @param uiProjection as passed from UnifiedEmail 2362 * @return the SQLite query to be executed on the EmailProvider database 2363 */ 2364 private String genQueryAttachment(String[] uiProjection) { 2365 StringBuilder sb = genSelect(sAttachmentMap, uiProjection); 2366 sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.ID + " =? "); 2367 return sb.toString(); 2368 } 2369 2370 /** 2371 * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail 2372 * 2373 * @param uiProjection as passed from UnifiedEmail 2374 * @return the SQLite query to be executed on the EmailProvider database 2375 */ 2376 private String genQuerySubfolders(String[] uiProjection) { 2377 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 2378 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY + 2379 " =? ORDER BY "); 2380 sb.append(MAILBOX_ORDER_BY); 2381 return sb.toString(); 2382 } 2383 2384 /** 2385 * Handle UnifiedEmail queries here (dispatched from query()) 2386 * 2387 * @param match the UriMatcher match for the original uri passed in from UnifiedEmail 2388 * @param uri the original uri passed in from UnifiedEmail 2389 * @param uiProjection the projection passed in from UnifiedEmail 2390 * @return the result Cursor 2391 */ 2392 private Cursor uiQuery(int match, Uri uri, String[] uiProjection) { 2393 Context context = getContext(); 2394 ContentResolver resolver = context.getContentResolver(); 2395 SQLiteDatabase db = getDatabase(context); 2396 // Should we ever return null, or throw an exception?? 2397 Cursor c = null; 2398 String id = uri.getPathSegments().get(1); 2399 Uri notifyUri = null; 2400 switch(match) { 2401 case UI_FOLDERS: 2402 c = db.rawQuery(genQueryAccountMailboxes(uiProjection), new String[] {id}); 2403 break; 2404 case UI_SUBFOLDERS: 2405 c = db.rawQuery(genQuerySubfolders(uiProjection), new String[] {id}); 2406 break; 2407 case UI_MESSAGES: 2408 c = db.rawQuery(genQueryMailboxMessages(uiProjection), new String[] {id}); 2409 notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build(); 2410 break; 2411 case UI_MESSAGE: 2412 c = db.rawQuery(genQueryViewMessage(uiProjection, id), new String[] {id}); 2413 break; 2414 case UI_ATTACHMENTS: 2415 c = db.rawQuery(genQueryAttachments(uiProjection), new String[] {id}); 2416 notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build(); 2417 break; 2418 case UI_ATTACHMENT: 2419 c = db.rawQuery(genQueryAttachment(uiProjection), new String[] {id}); 2420 notifyUri = UIPROVIDER_ATTACHMENT_NOTIFIER.buildUpon().appendPath(id).build(); 2421 break; 2422 case UI_FOLDER: 2423 c = db.rawQuery(genQueryMailbox(uiProjection, id), new String[] {id}); 2424 notifyUri = UIPROVIDER_MAILBOX_NOTIFIER.buildUpon().appendPath(id).build(); 2425 break; 2426 case UI_ACCOUNT: 2427 c = db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); 2428 notifyUri = UIPROVIDER_ACCOUNT_NOTIFIER.buildUpon().appendPath(id).build(); 2429 break; 2430 case UI_SETTINGS: 2431 c = db.rawQuery(genQuerySettings(uiProjection, id), new String[] {id}); 2432 notifyUri = UIPROVIDER_SETTINGS_NOTIFIER.buildUpon().appendPath(id).build(); 2433 break; 2434 } 2435 if (notifyUri != null) { 2436 c.setNotificationUri(resolver, notifyUri); 2437 } 2438 return c; 2439 } 2440 2441 /** 2442 * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need 2443 * a few of the fields 2444 * @param uiAtt the UIProvider attachment to convert 2445 * @return the EmailProvider attachment 2446 */ 2447 private Attachment convertUiAttachmentToAttachment( 2448 com.android.mail.providers.Attachment uiAtt) { 2449 Attachment att = new Attachment(); 2450 att.mContentUri = uiAtt.contentUri.toString(); 2451 att.mFileName = uiAtt.name; 2452 att.mMimeType = uiAtt.contentType; 2453 att.mSize = uiAtt.size; 2454 return att; 2455 } 2456 2457 /** 2458 * Create a mailbox given the account and mailboxType. 2459 */ 2460 private Mailbox createMailbox(long accountId, int mailboxType) { 2461 Context context = getContext(); 2462 int resId = -1; 2463 switch (mailboxType) { 2464 case Mailbox.TYPE_INBOX: 2465 resId = R.string.mailbox_name_server_inbox; 2466 break; 2467 case Mailbox.TYPE_OUTBOX: 2468 resId = R.string.mailbox_name_server_outbox; 2469 break; 2470 case Mailbox.TYPE_DRAFTS: 2471 resId = R.string.mailbox_name_server_drafts; 2472 break; 2473 case Mailbox.TYPE_TRASH: 2474 resId = R.string.mailbox_name_server_trash; 2475 break; 2476 case Mailbox.TYPE_SENT: 2477 resId = R.string.mailbox_name_server_sent; 2478 break; 2479 case Mailbox.TYPE_JUNK: 2480 resId = R.string.mailbox_name_server_junk; 2481 break; 2482 default: 2483 throw new IllegalArgumentException("Illegal mailbox type"); 2484 } 2485 Log.d(TAG, "Creating mailbox of type " + mailboxType + " for account " + accountId); 2486 Mailbox box = Mailbox.newSystemMailbox(accountId, mailboxType, context.getString(resId)); 2487 // Make sure drafts and save will show up in recents... 2488 // If these already exist (from old Email app), they will have touch times 2489 switch (mailboxType) { 2490 case Mailbox.TYPE_DRAFTS: 2491 box.mLastTouchedTime = Mailbox.DRAFTS_DEFAULT_TOUCH_TIME; 2492 break; 2493 case Mailbox.TYPE_SENT: 2494 box.mLastTouchedTime = Mailbox.SENT_DEFAULT_TOUCH_TIME; 2495 break; 2496 } 2497 box.save(context); 2498 return box; 2499 } 2500 2501 /** 2502 * Given an account name and a mailbox type, return that mailbox, creating it if necessary 2503 * @param accountName the account name to use 2504 * @param mailboxType the type of mailbox we're trying to find 2505 * @return the mailbox of the given type for the account in the uri, or null if not found 2506 */ 2507 private Mailbox getMailboxByAccountIdAndType(String accountId, int mailboxType) { 2508 long id = Long.parseLong(accountId); 2509 Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), id, mailboxType); 2510 if (mailbox == null) { 2511 mailbox = createMailbox(id, mailboxType); 2512 } 2513 return mailbox; 2514 } 2515 2516 private Message getMessageFromPathSegments(List<String> pathSegments) { 2517 Message msg = null; 2518 if (pathSegments.size() > 2) { 2519 msg = Message.restoreMessageWithId(getContext(), Long.parseLong(pathSegments.get(2))); 2520 } 2521 if (msg == null) { 2522 msg = new Message(); 2523 } 2524 return msg; 2525 } 2526 /** 2527 * Given a mailbox and the content values for a message, create/save the message in the mailbox 2528 * @param mailbox the mailbox to use 2529 * @param values the content values that represent message fields 2530 * @return the uri of the newly created message 2531 */ 2532 private Uri uiSaveMessage(Message msg, Mailbox mailbox, ContentValues values) { 2533 Context context = getContext(); 2534 // Fill in the message 2535 Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey); 2536 if (account == null) return null; 2537 msg.mFrom = account.mEmailAddress; 2538 msg.mTimeStamp = System.currentTimeMillis(); 2539 msg.mTo = values.getAsString(UIProvider.MessageColumns.TO); 2540 msg.mCc = values.getAsString(UIProvider.MessageColumns.CC); 2541 msg.mBcc = values.getAsString(UIProvider.MessageColumns.BCC); 2542 msg.mSubject = values.getAsString(UIProvider.MessageColumns.SUBJECT); 2543 msg.mText = values.getAsString(UIProvider.MessageColumns.BODY_TEXT); 2544 msg.mHtml = values.getAsString(UIProvider.MessageColumns.BODY_HTML); 2545 msg.mMailboxKey = mailbox.mId; 2546 msg.mAccountKey = mailbox.mAccountKey; 2547 msg.mDisplayName = msg.mTo; 2548 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 2549 // Get attachments from the ContentValues 2550 ArrayList<com.android.mail.providers.Attachment> uiAtts = 2551 com.android.mail.providers.Attachment.getAttachmentsFromJoinedAttachmentInfo( 2552 values.getAsString(UIProvider.MessageColumns.JOINED_ATTACHMENT_INFOS)); 2553 ArrayList<Attachment> atts = new ArrayList<Attachment>(); 2554 for (com.android.mail.providers.Attachment uiAtt: uiAtts) { 2555 // Convert to our attachments and add to the list; everything else should "just work" 2556 atts.add(convertUiAttachmentToAttachment(uiAtt)); 2557 } 2558 if (!atts.isEmpty()) { 2559 msg.mAttachments = atts; 2560 } 2561 // Save it or update it... 2562 if (!msg.isSaved()) { 2563 msg.save(context); 2564 } else { 2565 // This is tricky due to how messages/attachments are saved; rather than putz with 2566 // what's changed, we'll delete/re-add them 2567 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 2568 // Delete all existing attachments 2569 ops.add(ContentProviderOperation.newDelete( 2570 ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId)) 2571 .build()); 2572 // Delete the body 2573 ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI) 2574 .withSelection(Body.MESSAGE_KEY + "=?", new String[] {Long.toString(msg.mId)}) 2575 .build()); 2576 // Add the ops for the message, atts, and body 2577 msg.addSaveOps(ops); 2578 // Do it! 2579 try { 2580 applyBatch(ops); 2581 } catch (OperationApplicationException e) { 2582 } 2583 } 2584 if (mailbox.mType == Mailbox.TYPE_OUTBOX) { 2585 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 2586 mServiceCallback, mailbox.mAccountKey); 2587 try { 2588 service.startSync(mailbox.mId, true); 2589 } catch (RemoteException e) { 2590 } 2591 } 2592 return uiUri("uimessage", msg.mId); 2593 } 2594 2595 /** 2596 * Create and send the message via the account indicated in the uri 2597 * @param uri the incoming uri 2598 * @param values the content values that represent message fields 2599 * @return the uri of the created message 2600 */ 2601 private Uri uiSendMail(Uri uri, ContentValues values) { 2602 List<String> pathSegments = uri.getPathSegments(); 2603 Mailbox mailbox = getMailboxByAccountIdAndType(pathSegments.get(1), Mailbox.TYPE_OUTBOX); 2604 if (mailbox == null) return null; 2605 Message msg = getMessageFromPathSegments(pathSegments); 2606 try { 2607 return uiSaveMessage(msg, mailbox, values); 2608 } finally { 2609 // Kick observers 2610 getContext().getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 2611 } 2612 } 2613 2614 /** 2615 * Create a message and save it to the drafts folder of the account indicated in the uri 2616 * @param uri the incoming uri 2617 * @param values the content values that represent message fields 2618 * @return the uri of the created message 2619 */ 2620 private Uri uiSaveDraft(Uri uri, ContentValues values) { 2621 List<String> pathSegments = uri.getPathSegments(); 2622 Mailbox mailbox = getMailboxByAccountIdAndType(pathSegments.get(1), Mailbox.TYPE_DRAFTS); 2623 if (mailbox == null) return null; 2624 Message msg = getMessageFromPathSegments(pathSegments); 2625 return uiSaveMessage(msg, mailbox, values); 2626 } 2627 2628 private int uiUpdateDraft(Uri uri, ContentValues values) { 2629 Context context = getContext(); 2630 Message msg = Message.restoreMessageWithId(context, 2631 Long.parseLong(uri.getPathSegments().get(1))); 2632 if (msg == null) return 0; 2633 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 2634 if (mailbox == null) return 0; 2635 uiSaveMessage(msg, mailbox, values); 2636 return 1; 2637 } 2638 2639 private int uiSendDraft(Uri uri, ContentValues values) { 2640 Context context = getContext(); 2641 Message msg = Message.restoreMessageWithId(context, 2642 Long.parseLong(uri.getPathSegments().get(1))); 2643 if (msg == null) return 0; 2644 long mailboxId = Mailbox.findMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_OUTBOX); 2645 if (mailboxId == Mailbox.NO_MAILBOX) return 0; 2646 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 2647 if (mailbox == null) return 0; 2648 uiSaveMessage(msg, mailbox, values); 2649 // Kick observers 2650 context.getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 2651 return 1; 2652 } 2653 2654 private void putIntegerLongOrBoolean(ContentValues values, String columnName, Object value) { 2655 if (value instanceof Integer) { 2656 Integer intValue = (Integer)value; 2657 values.put(columnName, intValue); 2658 } else if (value instanceof Boolean) { 2659 Boolean boolValue = (Boolean)value; 2660 values.put(columnName, boolValue ? 1 : 0); 2661 } else if (value instanceof Long) { 2662 Long longValue = (Long)value; 2663 values.put(columnName, longValue); 2664 } 2665 } 2666 2667 private int uiUpdateAttachment(Uri uri, ContentValues uiValues) { 2668 Integer stateValue = uiValues.getAsInteger(UIProvider.AttachmentColumns.STATE); 2669 if (stateValue != null) { 2670 // This is a command from UIProvider 2671 long attachmentId = Long.parseLong(uri.getLastPathSegment()); 2672 Context context = getContext(); 2673 Attachment attachment = 2674 Attachment.restoreAttachmentWithId(context, attachmentId); 2675 if (attachment == null) { 2676 // Went away; ah, well... 2677 return 0; 2678 } 2679 ContentValues values = new ContentValues(); 2680 switch (stateValue.intValue()) { 2681 case UIProvider.AttachmentState.NOT_SAVED: 2682 // Set state, try to cancel request 2683 values.put(AttachmentColumns.UI_STATE, stateValue); 2684 values.put(AttachmentColumns.FLAGS, 2685 attachment.mFlags &= ~Attachment.FLAG_DOWNLOAD_USER_REQUEST); 2686 attachment.update(context, values); 2687 return 1; 2688 case UIProvider.AttachmentState.DOWNLOADING: 2689 // Set state and destination; request download 2690 values.put(AttachmentColumns.UI_STATE, stateValue); 2691 Integer destinationValue = 2692 uiValues.getAsInteger(UIProvider.AttachmentColumns.DESTINATION); 2693 values.put(AttachmentColumns.UI_DESTINATION, 2694 destinationValue == null ? 0 : destinationValue); 2695 values.put(AttachmentColumns.FLAGS, 2696 attachment.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST); 2697 attachment.update(context, values); 2698 return 1; 2699 } 2700 } 2701 return 0; 2702 } 2703 2704 private ContentValues convertUiMessageValues(ContentValues values) { 2705 ContentValues ourValues = new ContentValues(); 2706 for (String columnName: values.keySet()) { 2707 Object val = values.get(columnName); 2708 if (columnName.equals(UIProvider.ConversationColumns.STARRED)) { 2709 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val); 2710 } else if (columnName.equals(UIProvider.ConversationColumns.READ)) { 2711 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val); 2712 } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 2713 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val); 2714 } else if (columnName.equals(UIProvider.ConversationColumns.FOLDER_LIST)) { 2715 // Convert from folder list uri to mailbox key 2716 Uri uri = Uri.parse((String)val); 2717 Long mailboxId = Long.parseLong(uri.getLastPathSegment()); 2718 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId); 2719 } else { 2720 throw new IllegalArgumentException("Can't update " + columnName + " in message"); 2721 } 2722 } 2723 return ourValues; 2724 } 2725 2726 private Uri convertToEmailProviderUri(Uri uri, boolean asProvider) { 2727 String idString = uri.getLastPathSegment(); 2728 try { 2729 long id = Long.parseLong(idString); 2730 Uri ourUri = ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id); 2731 if (asProvider) { 2732 ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build(); 2733 } 2734 return ourUri; 2735 } catch (NumberFormatException e) { 2736 return null; 2737 } 2738 } 2739 2740 private Message getMessageFromLastSegment(Uri uri) { 2741 long messageId = Long.parseLong(uri.getLastPathSegment()); 2742 return Message.restoreMessageWithId(getContext(), messageId); 2743 } 2744 2745 /** 2746 * Add an undo operation for the current sequence; if the sequence is newer than what we've had, 2747 * clear out the undo list and start over 2748 * @param uri the uri we're working on 2749 * @param op the ContentProviderOperation to perform upon undo 2750 */ 2751 private void addToSequence(Uri uri, ContentProviderOperation op) { 2752 String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER); 2753 if (sequenceString != null) { 2754 int sequence = Integer.parseInt(sequenceString); 2755 if (sequence > mLastSequence) { 2756 // Reset sequence 2757 mLastSequenceOps.clear(); 2758 mLastSequence = sequence; 2759 } 2760 // TODO: Need something to indicate a change isn't ready (undoable) 2761 mLastSequenceOps.add(op); 2762 } 2763 } 2764 2765 private int uiUpdateMessage(Uri uri, ContentValues values) { 2766 Uri ourUri = convertToEmailProviderUri(uri, true); 2767 if (ourUri == null) return 0; 2768 ContentValues ourValues = convertUiMessageValues(values); 2769 Message msg = getMessageFromLastSegment(uri); 2770 if (msg == null) return 0; 2771 ContentValues undoValues = new ContentValues(); 2772 for (String columnName: ourValues.keySet()) { 2773 if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 2774 undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey); 2775 } else if (columnName.equals(MessageColumns.FLAG_READ)) { 2776 undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead); 2777 } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) { 2778 undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite); 2779 } 2780 } 2781 ContentProviderOperation op = 2782 ContentProviderOperation.newUpdate(convertToEmailProviderUri(uri, false)) 2783 .withValues(undoValues) 2784 .build(); 2785 addToSequence(uri, op); 2786 return update(ourUri, ourValues, null, null); 2787 } 2788 2789 private int uiDeleteMessage(Uri uri) { 2790 Context context = getContext(); 2791 Message msg = getMessageFromLastSegment(uri); 2792 if (msg == null) return 0; 2793 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 2794 if (mailbox == null) return 0; 2795 if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_DRAFTS) { 2796 // We actually delete these, including attachments 2797 AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId); 2798 return context.getContentResolver().delete( 2799 ContentUris.withAppendedId(Message.CONTENT_URI, msg.mId), null, null); 2800 } 2801 Mailbox trashMailbox = 2802 Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH); 2803 if (trashMailbox == null) return 0; 2804 ContentValues values = new ContentValues(); 2805 values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId); 2806 return uiUpdateMessage(uri, values); 2807 } 2808 2809 private Cursor uiUndo(Uri uri, String[] projection) { 2810 // First see if we have any operations saved 2811 // TODO: Make sure seq matches 2812 if (!mLastSequenceOps.isEmpty()) { 2813 try { 2814 // TODO Always use this projection? Or what's passed in? 2815 // Not sure if UI wants it, but I'm making a cursor of convo uri's 2816 MatrixCursor c = new MatrixCursor( 2817 new String[] {UIProvider.ConversationColumns.URI}, 2818 mLastSequenceOps.size()); 2819 for (ContentProviderOperation op: mLastSequenceOps) { 2820 c.addRow(new String[] {op.getUri().toString()}); 2821 } 2822 // Just apply the batch and we're done! 2823 applyBatch(mLastSequenceOps); 2824 // But clear the operations 2825 mLastSequenceOps.clear(); 2826 // Tell the UI there are changes 2827 getContext().getContentResolver().notifyChange(UIPROVIDER_CONVERSATION_NOTIFIER, 2828 null); 2829 Log.d(TAG, "[Notify UI: Undo]"); 2830 return c; 2831 } catch (OperationApplicationException e) { 2832 } 2833 } 2834 return new MatrixCursor(projection, 0); 2835 } 2836 2837 private void notifyUIConversation(Uri uri) { 2838 String id = uri.getLastPathSegment(); 2839 Message msg = Message.restoreMessageWithId(getContext(), Long.parseLong(id)); 2840 if (msg != null) { 2841 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(msg.mMailboxKey)); 2842 } 2843 } 2844 2845 private void notifyUIConversationMailbox(long id) { 2846 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(id)); 2847 } 2848 2849 private void notifyUI(Uri uri, String id) { 2850 Uri notifyUri = uri.buildUpon().appendPath(id).build(); 2851 getContext().getContentResolver().notifyChange(notifyUri, null); 2852 // Temporary 2853 Log.d(TAG, "[Notify UI: " + notifyUri + "]"); 2854 } 2855 2856 private void notifyUI(Uri uri, long id) { 2857 notifyUI(uri, Long.toString(id)); 2858 } 2859 2860 /** 2861 * Support for services and service notifications 2862 */ 2863 2864 private final IEmailServiceCallback.Stub mServiceCallback = 2865 new IEmailServiceCallback.Stub() { 2866 2867 @Override 2868 public void syncMailboxListStatus(long accountId, int statusCode, int progress) 2869 throws RemoteException { 2870 } 2871 2872 @Override 2873 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) 2874 throws RemoteException { 2875 // We'll get callbacks here from the services, which we'll pass back to the UI 2876 Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, mailboxId); 2877 EmailProvider.this.getContext().getContentResolver().notifyChange(uri, null); 2878 } 2879 2880 @Override 2881 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 2882 int progress) throws RemoteException { 2883 } 2884 2885 @Override 2886 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) 2887 throws RemoteException { 2888 } 2889 2890 @Override 2891 public void loadMessageStatus(long messageId, int statusCode, int progress) 2892 throws RemoteException { 2893 } 2894 }; 2895 2896 private Cursor uiFolderRefresh(Uri uri) { 2897 Context context = getContext(); 2898 String idString = uri.getLastPathSegment(); 2899 long id = Long.parseLong(idString); 2900 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, id); 2901 if (mailbox == null) return null; 2902 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 2903 mServiceCallback, mailbox.mAccountKey); 2904 try { 2905 service.startSync(id, true); 2906 } catch (RemoteException e) { 2907 } 2908 return null; 2909 } 2910 2911 //Number of additional messages to load when a user selects "Load more..." in POP/IMAP boxes 2912 public static final int VISIBLE_LIMIT_INCREMENT = 10; 2913 //Number of additional messages to load when a user selects "Load more..." in a search 2914 public static final int SEARCH_MORE_INCREMENT = 10; 2915 2916 private Cursor uiFolderLoadMore(Uri uri) { 2917 Context context = getContext(); 2918 String idString = uri.getLastPathSegment(); 2919 long id = Long.parseLong(idString); 2920 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, id); 2921 if (mailbox == null) return null; 2922 if (mailbox.mType == Mailbox.TYPE_SEARCH) { 2923 // Ask for 10 more messages 2924 mSearchParams.mOffset += SEARCH_MORE_INCREMENT; 2925 runSearchQuery(context, mailbox.mAccountKey, id); 2926 } else { 2927 ContentValues values = new ContentValues(); 2928 values.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT); 2929 values.put(EmailContent.ADD_COLUMN_NAME, VISIBLE_LIMIT_INCREMENT); 2930 Uri mailboxUri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, id); 2931 // Increase the limit 2932 context.getContentResolver().update(mailboxUri, values, null, null); 2933 // And order a refresh 2934 uiFolderRefresh(uri); 2935 } 2936 return null; 2937 } 2938 2939 private static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__"; 2940 private SearchParams mSearchParams; 2941 2942 /** 2943 * Returns the search mailbox for the specified account, creating one if necessary 2944 * @return the search mailbox for the passed in account 2945 */ 2946 private Mailbox getSearchMailbox(long accountId) { 2947 Context context = getContext(); 2948 Mailbox m = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SEARCH); 2949 if (m == null) { 2950 m = new Mailbox(); 2951 m.mAccountKey = accountId; 2952 m.mServerId = SEARCH_MAILBOX_SERVER_ID; 2953 m.mFlagVisible = false; 2954 m.mDisplayName = SEARCH_MAILBOX_SERVER_ID; 2955 m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 2956 m.mType = Mailbox.TYPE_SEARCH; 2957 m.mFlags = Mailbox.FLAG_HOLDS_MAIL; 2958 m.mParentKey = Mailbox.NO_MAILBOX; 2959 m.save(context); 2960 } 2961 return m; 2962 } 2963 2964 private void runSearchQuery(final Context context, final long accountId, 2965 final long searchMailboxId) { 2966 // Start the search running in the background 2967 new Thread(new Runnable() { 2968 @Override 2969 public void run() { 2970 try { 2971 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 2972 mServiceCallback, accountId); 2973 if (service != null) { 2974 try { 2975 // Save away the total count 2976 mSearchParams.mTotalCount = service.searchMessages(accountId, 2977 mSearchParams, searchMailboxId); 2978 Log.d(TAG, "TotalCount to UI: " + mSearchParams.mTotalCount); 2979 notifyUI(UIPROVIDER_MAILBOX_NOTIFIER, searchMailboxId); 2980 } catch (RemoteException e) { 2981 Log.e("searchMessages", "RemoteException", e); 2982 } 2983 } 2984 } finally { 2985 } 2986 }}).start(); 2987 2988 } 2989 2990 // TODO: Handle searching for more... 2991 private Cursor uiSearch(Uri uri, String[] projection) { 2992 final long accountId = Long.parseLong(uri.getLastPathSegment()); 2993 2994 // TODO: Check the actual mailbox 2995 Mailbox inbox = Mailbox.restoreMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX); 2996 if (inbox == null) return null; 2997 2998 String filter = uri.getQueryParameter(UIProvider.SearchQueryParameters.QUERY); 2999 if (filter == null) { 3000 throw new IllegalArgumentException("No query parameter in search query"); 3001 } 3002 3003 // Find/create our search mailbox 3004 Mailbox searchMailbox = getSearchMailbox(accountId); 3005 final long searchMailboxId = searchMailbox.mId; 3006 3007 mSearchParams = new SearchParams(inbox.mId, filter, searchMailboxId); 3008 3009 final Context context = getContext(); 3010 if (mSearchParams.mOffset == 0) { 3011 // Delete existing contents of search mailbox 3012 ContentResolver resolver = context.getContentResolver(); 3013 resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId, 3014 null); 3015 ContentValues cv = new ContentValues(); 3016 // For now, use the actual query as the name of the mailbox 3017 cv.put(Mailbox.DISPLAY_NAME, mSearchParams.mFilter); 3018 resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), 3019 cv, null, null); 3020 } 3021 3022 // Start the search running in the background 3023 runSearchQuery(context, accountId, searchMailboxId); 3024 3025 // This will look just like a "normal" folder 3026 return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI, 3027 searchMailbox.mId), projection); 3028 } 3029 3030 private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; 3031 private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION = 3032 MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" + 3033 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX; 3034 private static final String MESSAGES_FOR_ACCOUNT_SELECTION = MessageColumns.ACCOUNT_KEY + "=?"; 3035 3036 /** 3037 * Delete an account and clean it up 3038 */ 3039 private int uiDeleteAccount(Uri uri) { 3040 Context context = getContext(); 3041 long accountId = Long.parseLong(uri.getLastPathSegment()); 3042 try { 3043 // Get the account URI. 3044 final Account account = Account.restoreAccountWithId(context, accountId); 3045 if (account == null) { 3046 return 0; // Already deleted? 3047 } 3048 3049 deleteAccountData(context, accountId); 3050 3051 // Now delete the account itself 3052 uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 3053 context.getContentResolver().delete(uri, null, null); 3054 3055 // Clean up 3056 AccountBackupRestore.backup(context); 3057 SecurityPolicy.getInstance(context).reducePolicies(); 3058 Email.setServicesEnabledSync(context); 3059 return 1; 3060 } catch (Exception e) { 3061 Log.w(Logging.LOG_TAG, "Exception while deleting account", e); 3062 } 3063 return 0; 3064 } 3065 3066 private int uiDeleteAccountData(Uri uri) { 3067 Context context = getContext(); 3068 long accountId = Long.parseLong(uri.getLastPathSegment()); 3069 // Get the account URI. 3070 final Account account = Account.restoreAccountWithId(context, accountId); 3071 if (account == null) { 3072 return 0; // Already deleted? 3073 } 3074 deleteAccountData(context, accountId); 3075 return 1; 3076 } 3077 3078 private void deleteAccountData(Context context, long accountId) { 3079 // Delete synced attachments 3080 AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId); 3081 3082 // Delete synced email, leaving only an empty inbox. We do this in two phases: 3083 // 1. Delete all non-inbox mailboxes (which will delete all of their messages) 3084 // 2. Delete all remaining messages (which will be the inbox messages) 3085 ContentResolver resolver = context.getContentResolver(); 3086 String[] accountIdArgs = new String[] { Long.toString(accountId) }; 3087 resolver.delete(Mailbox.CONTENT_URI, 3088 MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION, 3089 accountIdArgs); 3090 resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs); 3091 3092 // Delete sync keys on remaining items 3093 ContentValues cv = new ContentValues(); 3094 cv.putNull(Account.SYNC_KEY); 3095 resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs); 3096 cv.clear(); 3097 cv.putNull(Mailbox.SYNC_KEY); 3098 resolver.update(Mailbox.CONTENT_URI, cv, 3099 MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs); 3100 3101 // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable 3102 IEmailService service = EmailServiceUtils.getServiceForAccount(context, null, accountId); 3103 if (service != null) { 3104 try { 3105 service.deleteAccountPIMData(accountId); 3106 } catch (RemoteException e) { 3107 // Can't do anything about this 3108 } 3109 } 3110 } 3111} 3112