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