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