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 17 package com.android.email.provider; 18 19 import android.content.ContentProvider; 20 import android.content.ContentProviderOperation; 21 import android.content.ContentProviderResult; 22 import android.content.ContentResolver; 23 import android.content.ContentUris; 24 import android.content.ContentValues; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.OperationApplicationException; 28 import android.content.UriMatcher; 29 import android.database.ContentObserver; 30 import android.database.Cursor; 31 import android.database.MatrixCursor; 32 import android.database.sqlite.SQLiteDatabase; 33 import android.database.sqlite.SQLiteException; 34 import android.net.Uri; 35 import android.text.TextUtils; 36 import android.util.Log; 37 38 import com.android.email.Email; 39 import com.android.email.Preferences; 40 import com.android.email.provider.ContentCache.CacheToken; 41 import com.android.email.service.AttachmentDownloadService; 42 import com.android.emailcommon.Logging; 43 import com.android.emailcommon.provider.Account; 44 import com.android.emailcommon.provider.EmailContent; 45 import com.android.emailcommon.provider.EmailContent.AccountColumns; 46 import com.android.emailcommon.provider.EmailContent.Attachment; 47 import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 48 import com.android.emailcommon.provider.EmailContent.Body; 49 import com.android.emailcommon.provider.EmailContent.BodyColumns; 50 import com.android.emailcommon.provider.EmailContent.MailboxColumns; 51 import com.android.emailcommon.provider.EmailContent.Message; 52 import com.android.emailcommon.provider.EmailContent.MessageColumns; 53 import com.android.emailcommon.provider.EmailContent.PolicyColumns; 54 import com.android.emailcommon.provider.HostAuth; 55 import com.android.emailcommon.provider.Mailbox; 56 import com.android.emailcommon.provider.Policy; 57 import com.android.emailcommon.provider.QuickResponse; 58 import com.google.common.annotations.VisibleForTesting; 59 60 import java.io.File; 61 import java.util.ArrayList; 62 import java.util.Arrays; 63 import java.util.Collection; 64 import java.util.HashMap; 65 import java.util.List; 66 import java.util.Map; 67 68 /** 69 * @author mblank 70 * 71 */ 72 public class EmailProvider extends ContentProvider { 73 74 private static final String TAG = "EmailProvider"; 75 76 public static final String EMAIL_APP_MIME_TYPE = "application/email-ls"; 77 78 protected static final String DATABASE_NAME = "EmailProvider.db"; 79 protected static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; 80 protected static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db"; 81 82 public static final String ACTION_ATTACHMENT_UPDATED = "com.android.email.ATTACHMENT_UPDATED"; 83 public static final String ATTACHMENT_UPDATED_EXTRA_FLAGS = 84 "com.android.email.ATTACHMENT_UPDATED_FLAGS"; 85 86 /** 87 * Notifies that changes happened. Certain UI components, e.g., widgets, can register for this 88 * {@link android.content.Intent} and update accordingly. However, this can be very broad and 89 * is NOT the preferred way of getting notification. 90 */ 91 public static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED = 92 "com.android.email.MESSAGE_LIST_DATASET_CHANGED"; 93 94 public static final String EMAIL_MESSAGE_MIME_TYPE = 95 "vnd.android.cursor.item/email-message"; 96 public static final String EMAIL_ATTACHMENT_MIME_TYPE = 97 "vnd.android.cursor.item/email-attachment"; 98 99 public static final Uri INTEGRITY_CHECK_URI = 100 Uri.parse("content://" + EmailContent.AUTHORITY + "/integrityCheck"); 101 public static final Uri ACCOUNT_BACKUP_URI = 102 Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup"); 103 public static final Uri FOLDER_STATUS_URI = 104 Uri.parse("content://" + EmailContent.AUTHORITY + "/status"); 105 public static final Uri FOLDER_REFRESH_URI = 106 Uri.parse("content://" + EmailContent.AUTHORITY + "/refresh"); 107 108 /** Appended to the notification URI for delete operations */ 109 public static final String NOTIFICATION_OP_DELETE = "delete"; 110 /** Appended to the notification URI for insert operations */ 111 public static final String NOTIFICATION_OP_INSERT = "insert"; 112 /** Appended to the notification URI for update operations */ 113 public static final String NOTIFICATION_OP_UPDATE = "update"; 114 115 // Definitions for our queries looking for orphaned messages 116 private static final String[] ORPHANS_PROJECTION 117 = new String[] {MessageColumns.ID, MessageColumns.MAILBOX_KEY}; 118 private static final int ORPHANS_ID = 0; 119 private static final int ORPHANS_MAILBOX_KEY = 1; 120 121 private static final String WHERE_ID = EmailContent.RECORD_ID + "=?"; 122 123 // This is not a hard limit on accounts, per se, but beyond this, we can't guarantee that all 124 // critical mailboxes, host auth's, accounts, and policies are cached 125 private static final int MAX_CACHED_ACCOUNTS = 16; 126 // Inbox, Drafts, Sent, Outbox, Trash, and Search (these boxes are cached when possible) 127 private static final int NUM_ALWAYS_CACHED_MAILBOXES = 6; 128 129 // We'll cache the following four tables; sizes are best estimates of effective values 130 private final ContentCache mCacheAccount = 131 new ContentCache("Account", Account.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS); 132 private final ContentCache mCacheHostAuth = 133 new ContentCache("HostAuth", HostAuth.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS * 2); 134 /*package*/ final ContentCache mCacheMailbox = 135 new ContentCache("Mailbox", Mailbox.CONTENT_PROJECTION, 136 MAX_CACHED_ACCOUNTS * (NUM_ALWAYS_CACHED_MAILBOXES + 2)); 137 private final ContentCache mCacheMessage = 138 new ContentCache("Message", Message.CONTENT_PROJECTION, 8); 139 private final ContentCache mCachePolicy = 140 new ContentCache("Policy", Policy.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS); 141 142 private static final int ACCOUNT_BASE = 0; 143 private static final int ACCOUNT = ACCOUNT_BASE; 144 private static final int ACCOUNT_ID = ACCOUNT_BASE + 1; 145 private static final int ACCOUNT_ID_ADD_TO_FIELD = ACCOUNT_BASE + 2; 146 private static final int ACCOUNT_RESET_NEW_COUNT = ACCOUNT_BASE + 3; 147 private static final int ACCOUNT_RESET_NEW_COUNT_ID = ACCOUNT_BASE + 4; 148 private static final int ACCOUNT_DEFAULT_ID = ACCOUNT_BASE + 5; 149 150 private static final int MAILBOX_BASE = 0x1000; 151 private static final int MAILBOX = MAILBOX_BASE; 152 private static final int MAILBOX_ID = MAILBOX_BASE + 1; 153 private static final int MAILBOX_ID_FROM_ACCOUNT_AND_TYPE = MAILBOX_BASE + 2; 154 private static final int MAILBOX_ID_ADD_TO_FIELD = MAILBOX_BASE + 3; 155 private static final int MAILBOX_NOTIFICATION = MAILBOX_BASE + 4; 156 private static final int MAILBOX_MOST_RECENT_MESSAGE = MAILBOX_BASE + 5; 157 158 private static final int MESSAGE_BASE = 0x2000; 159 private static final int MESSAGE = MESSAGE_BASE; 160 private static final int MESSAGE_ID = MESSAGE_BASE + 1; 161 private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2; 162 163 private static final int ATTACHMENT_BASE = 0x3000; 164 private static final int ATTACHMENT = ATTACHMENT_BASE; 165 private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1; 166 private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2; 167 168 private static final int HOSTAUTH_BASE = 0x4000; 169 private static final int HOSTAUTH = HOSTAUTH_BASE; 170 private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1; 171 172 private static final int UPDATED_MESSAGE_BASE = 0x5000; 173 private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE; 174 private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1; 175 176 private static final int DELETED_MESSAGE_BASE = 0x6000; 177 private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE; 178 private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1; 179 180 private static final int POLICY_BASE = 0x7000; 181 private static final int POLICY = POLICY_BASE; 182 private static final int POLICY_ID = POLICY_BASE + 1; 183 184 private static final int QUICK_RESPONSE_BASE = 0x8000; 185 private static final int QUICK_RESPONSE = QUICK_RESPONSE_BASE; 186 private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1; 187 private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2; 188 189 private static final int UI_BASE = 0x9000; 190 private static final int UI_FOLDERS = UI_BASE; 191 private static final int UI_SUBFOLDERS = UI_BASE + 1; 192 private static final int UI_MESSAGES = UI_BASE + 2; 193 private static final int UI_MESSAGE = UI_BASE + 3; 194 private static final int UI_SENDMAIL = UI_BASE + 4; 195 private static final int UI_UNDO = UI_BASE + 5; 196 private static final int UI_SAVEDRAFT = UI_BASE + 6; 197 private static final int UI_UPDATEDRAFT = UI_BASE + 7; 198 private static final int UI_SENDDRAFT = UI_BASE + 8; 199 private static final int UI_FOLDER_REFRESH = UI_BASE + 9; 200 private static final int UI_FOLDER = UI_BASE + 10; 201 private static final int UI_ACCOUNT = UI_BASE + 11; 202 private static final int UI_ACCTS = UI_BASE + 12; 203 private static final int UI_ATTACHMENTS = UI_BASE + 13; 204 private static final int UI_ATTACHMENT = UI_BASE + 14; 205 private static final int UI_SEARCH = UI_BASE + 15; 206 private static final int UI_ACCOUNT_DATA = UI_BASE + 16; 207 private static final int UI_FOLDER_LOAD_MORE = UI_BASE + 17; 208 private static final int UI_CONVERSATION = UI_BASE + 18; 209 private static final int UI_RECENT_FOLDERS = UI_BASE + 19; 210 211 // MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS 212 private static final int LAST_EMAIL_PROVIDER_DB_BASE = UI_BASE; 213 214 // DO NOT CHANGE BODY_BASE!! 215 private static final int BODY_BASE = LAST_EMAIL_PROVIDER_DB_BASE + 0x1000; 216 private static final int BODY = BODY_BASE; 217 private static final int BODY_ID = BODY_BASE + 1; 218 219 private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc. 220 221 // TABLE_NAMES MUST remain in the order of the BASE constants above (e.g. ACCOUNT_BASE = 0x0000, 222 // MESSAGE_BASE = 0x1000, etc.) 223 private static final String[] TABLE_NAMES = { 224 Account.TABLE_NAME, 225 Mailbox.TABLE_NAME, 226 Message.TABLE_NAME, 227 Attachment.TABLE_NAME, 228 HostAuth.TABLE_NAME, 229 Message.UPDATED_TABLE_NAME, 230 Message.DELETED_TABLE_NAME, 231 Policy.TABLE_NAME, 232 QuickResponse.TABLE_NAME, 233 null, // UI 234 Body.TABLE_NAME, 235 }; 236 237 // CONTENT_CACHES MUST remain in the order of the BASE constants above 238 private final ContentCache[] mContentCaches = { 239 mCacheAccount, 240 mCacheMailbox, 241 mCacheMessage, 242 null, // Attachment 243 mCacheHostAuth, 244 null, // Updated message 245 null, // Deleted message 246 mCachePolicy, 247 null, // Quick response 248 null, // Body 249 null // UI 250 }; 251 252 // CACHE_PROJECTIONS MUST remain in the order of the BASE constants above 253 private static final String[][] CACHE_PROJECTIONS = { 254 Account.CONTENT_PROJECTION, 255 Mailbox.CONTENT_PROJECTION, 256 Message.CONTENT_PROJECTION, 257 null, // Attachment 258 HostAuth.CONTENT_PROJECTION, 259 null, // Updated message 260 null, // Deleted message 261 Policy.CONTENT_PROJECTION, 262 null, // Quick response 263 null, // Body 264 null // UI 265 }; 266 267 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 268 269 private static final String MAILBOX_PRE_CACHE_SELECTION = MailboxColumns.TYPE + " IN (" + 270 Mailbox.TYPE_INBOX + "," + Mailbox.TYPE_DRAFTS + "," + Mailbox.TYPE_TRASH + "," + 271 Mailbox.TYPE_SENT + "," + Mailbox.TYPE_SEARCH + "," + Mailbox.TYPE_OUTBOX + ")"; 272 273 /** 274 * Let's only generate these SQL strings once, as they are used frequently 275 * Note that this isn't relevant for table creation strings, since they are used only once 276 */ 277 private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " + 278 Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 279 EmailContent.RECORD_ID + '='; 280 281 private static final String UPDATED_MESSAGE_DELETE = "delete from " + 282 Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '='; 283 284 private static final String DELETED_MESSAGE_INSERT = "insert or replace into " + 285 Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 286 EmailContent.RECORD_ID + '='; 287 288 private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME + 289 " where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY + 290 " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " + 291 Message.TABLE_NAME + ')'; 292 293 private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME + 294 " where " + BodyColumns.MESSAGE_KEY + '='; 295 296 private static final String ID_EQUALS = EmailContent.RECORD_ID + "=?"; 297 298 private static final ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 299 300 public static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; 301 302 static { 303 // Email URI matching table 304 UriMatcher matcher = sURIMatcher; 305 306 // All accounts 307 matcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT); 308 // A specific account 309 // insert into this URI causes a mailbox to be added to the account 310 matcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID); 311 matcher.addURI(EmailContent.AUTHORITY, "account/default", ACCOUNT_DEFAULT_ID); 312 313 // Special URI to reset the new message count. Only update works, and content values 314 // will be ignored. 315 matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount", 316 ACCOUNT_RESET_NEW_COUNT); 317 matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount/#", 318 ACCOUNT_RESET_NEW_COUNT_ID); 319 320 // All mailboxes 321 matcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX); 322 // A specific mailbox 323 // insert into this URI causes a message to be added to the mailbox 324 // ** NOTE For now, the accountKey must be set manually in the values! 325 matcher.addURI(EmailContent.AUTHORITY, "mailbox/#", MAILBOX_ID); 326 matcher.addURI(EmailContent.AUTHORITY, "mailboxIdFromAccountAndType/#/#", 327 MAILBOX_ID_FROM_ACCOUNT_AND_TYPE); 328 matcher.addURI(EmailContent.AUTHORITY, "mailboxNotification/#", MAILBOX_NOTIFICATION); 329 matcher.addURI(EmailContent.AUTHORITY, "mailboxMostRecentMessage/#", 330 MAILBOX_MOST_RECENT_MESSAGE); 331 332 // All messages 333 matcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE); 334 // A specific message 335 // insert into this URI causes an attachment to be added to the message 336 matcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID); 337 338 // A specific attachment 339 matcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT); 340 // A specific attachment (the header information) 341 matcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID); 342 // The attachments of a specific message (query only) (insert & delete TBD) 343 matcher.addURI(EmailContent.AUTHORITY, "attachment/message/#", 344 ATTACHMENTS_MESSAGE_ID); 345 346 // All mail bodies 347 matcher.addURI(EmailContent.AUTHORITY, "body", BODY); 348 // A specific mail body 349 matcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID); 350 351 // All hostauth records 352 matcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH); 353 // A specific hostauth 354 matcher.addURI(EmailContent.AUTHORITY, "hostauth/#", HOSTAUTH_ID); 355 356 // Atomically a constant value to a particular field of a mailbox/account 357 matcher.addURI(EmailContent.AUTHORITY, "mailboxIdAddToField/#", 358 MAILBOX_ID_ADD_TO_FIELD); 359 matcher.addURI(EmailContent.AUTHORITY, "accountIdAddToField/#", 360 ACCOUNT_ID_ADD_TO_FIELD); 361 362 /** 363 * THIS URI HAS SPECIAL SEMANTICS 364 * ITS USE IS INTENDED FOR THE UI APPLICATION TO MARK CHANGES THAT NEED TO BE SYNCED BACK 365 * TO A SERVER VIA A SYNC ADAPTER 366 */ 367 matcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); 368 369 /** 370 * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY 371 * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI 372 * BY THE UI APPLICATION 373 */ 374 // All deleted messages 375 matcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE); 376 // A specific deleted message 377 matcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID); 378 379 // All updated messages 380 matcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE); 381 // A specific updated message 382 matcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID); 383 384 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues(); 385 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0); 386 387 matcher.addURI(EmailContent.AUTHORITY, "policy", POLICY); 388 matcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID); 389 390 // All quick responses 391 matcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE); 392 // A specific quick response 393 matcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID); 394 // All quick responses associated with a particular account id 395 matcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#", 396 QUICK_RESPONSE_ACCOUNT_ID); 397 398 matcher.addURI(EmailContent.AUTHORITY, "uifolders/#", UI_FOLDERS); 399 matcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS); 400 matcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES); 401 matcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE); 402 matcher.addURI(EmailContent.AUTHORITY, "uisendmail/#", UI_SENDMAIL); 403 matcher.addURI(EmailContent.AUTHORITY, "uiundo", UI_UNDO); 404 matcher.addURI(EmailContent.AUTHORITY, "uisavedraft/#", UI_SAVEDRAFT); 405 matcher.addURI(EmailContent.AUTHORITY, "uiupdatedraft/#", UI_UPDATEDRAFT); 406 matcher.addURI(EmailContent.AUTHORITY, "uisenddraft/#", UI_SENDDRAFT); 407 matcher.addURI(EmailContent.AUTHORITY, "uirefresh/#", UI_FOLDER_REFRESH); 408 matcher.addURI(EmailContent.AUTHORITY, "uifolder/#", UI_FOLDER); 409 matcher.addURI(EmailContent.AUTHORITY, "uiaccount/#", UI_ACCOUNT); 410 matcher.addURI(EmailContent.AUTHORITY, "uiaccts", UI_ACCTS); 411 matcher.addURI(EmailContent.AUTHORITY, "uiattachments/#", UI_ATTACHMENTS); 412 matcher.addURI(EmailContent.AUTHORITY, "uiattachment/#", UI_ATTACHMENT); 413 matcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH); 414 matcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA); 415 matcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE); 416 matcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION); 417 matcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS); 418 } 419 420 /** 421 * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in 422 * @param uri the Uri to match 423 * @return the match value 424 */ 425 private static int findMatch(Uri uri, String methodName) { 426 int match = sURIMatcher.match(uri); 427 if (match < 0) { 428 throw new IllegalArgumentException("Unknown uri: " + uri); 429 } else if (Logging.LOGD) { 430 Log.v(TAG, methodName + ": uri=" + uri + ", match is " + match); 431 } 432 return match; 433 } 434 435 private SQLiteDatabase mDatabase; 436 private SQLiteDatabase mBodyDatabase; 437 438 public static Uri uiUri(String type, long id) { 439 return Uri.parse(uiUriString(type, id)); 440 } 441 442 public static String uiUriString(String type, long id) { 443 return "content://" + EmailContent.AUTHORITY + "/" + type + ((id == -1) ? "" : ("/" + id)); 444 } 445 446 /** 447 * Orphan record deletion utility. Generates a sqlite statement like: 448 * delete from <table> where <column> not in (select <foreignColumn> from <foreignTable>) 449 * @param db the EmailProvider database 450 * @param table the table whose orphans are to be removed 451 * @param column the column deletion will be based on 452 * @param foreignColumn the column in the foreign table whose absence will trigger the deletion 453 * @param foreignTable the foreign table 454 */ 455 @VisibleForTesting 456 void deleteUnlinked(SQLiteDatabase db, String table, String column, String foreignColumn, 457 String foreignTable) { 458 int count = db.delete(table, column + " not in (select " + foreignColumn + " from " + 459 foreignTable + ")", null); 460 if (count > 0) { 461 Log.w(TAG, "Found " + count + " orphaned row(s) in " + table); 462 } 463 } 464 465 @VisibleForTesting 466 synchronized SQLiteDatabase getDatabase(Context context) { 467 // Always return the cached database, if we've got one 468 if (mDatabase != null) { 469 return mDatabase; 470 } 471 472 // Whenever we create or re-cache the databases, make sure that we haven't lost one 473 // to corruption 474 checkDatabases(); 475 476 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); 477 mDatabase = helper.getWritableDatabase(); 478 DBHelper.BodyDatabaseHelper bodyHelper = 479 new DBHelper.BodyDatabaseHelper(context, BODY_DATABASE_NAME); 480 mBodyDatabase = bodyHelper.getWritableDatabase(); 481 if (mBodyDatabase != null) { 482 String bodyFileName = mBodyDatabase.getPath(); 483 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); 484 } 485 486 // Restore accounts if the database is corrupted... 487 restoreIfNeeded(context, mDatabase); 488 // Check for any orphaned Messages in the updated/deleted tables 489 deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME); 490 deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME); 491 // Delete orphaned mailboxes/messages/policies (account no longer exists) 492 deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY, AccountColumns.ID, 493 Account.TABLE_NAME); 494 deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY, AccountColumns.ID, 495 Account.TABLE_NAME); 496 deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns.ID, AccountColumns.POLICY_KEY, 497 Account.TABLE_NAME); 498 preCacheData(); 499 return mDatabase; 500 } 501 502 /** 503 * Pre-cache all of the items in a given table meeting the selection criteria 504 * @param tableUri the table uri 505 * @param baseProjection the base projection of that table 506 * @param selection the selection criteria 507 */ 508 private void preCacheTable(Uri tableUri, String[] baseProjection, String selection) { 509 Cursor c = query(tableUri, EmailContent.ID_PROJECTION, selection, null, null); 510 try { 511 while (c.moveToNext()) { 512 long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 513 Cursor cachedCursor = query(ContentUris.withAppendedId( 514 tableUri, id), baseProjection, null, null, null); 515 if (cachedCursor != null) { 516 // For accounts, create a mailbox type map entry (if necessary) 517 if (tableUri == Account.CONTENT_URI) { 518 getOrCreateAccountMailboxTypeMap(id); 519 } 520 cachedCursor.close(); 521 } 522 } 523 } finally { 524 c.close(); 525 } 526 } 527 528 private final HashMap<Long, HashMap<Integer, Long>> mMailboxTypeMap = 529 new HashMap<Long, HashMap<Integer, Long>>(); 530 531 private HashMap<Integer, Long> getOrCreateAccountMailboxTypeMap(long accountId) { 532 synchronized(mMailboxTypeMap) { 533 HashMap<Integer, Long> accountMailboxTypeMap = mMailboxTypeMap.get(accountId); 534 if (accountMailboxTypeMap == null) { 535 accountMailboxTypeMap = new HashMap<Integer, Long>(); 536 mMailboxTypeMap.put(accountId, accountMailboxTypeMap); 537 } 538 return accountMailboxTypeMap; 539 } 540 } 541 542 private void addToMailboxTypeMap(Cursor c) { 543 long accountId = c.getLong(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN); 544 int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 545 synchronized(mMailboxTypeMap) { 546 HashMap<Integer, Long> accountMailboxTypeMap = 547 getOrCreateAccountMailboxTypeMap(accountId); 548 accountMailboxTypeMap.put(type, c.getLong(Mailbox.CONTENT_ID_COLUMN)); 549 } 550 } 551 552 private long getMailboxIdFromMailboxTypeMap(long accountId, int type) { 553 synchronized(mMailboxTypeMap) { 554 HashMap<Integer, Long> accountMap = mMailboxTypeMap.get(accountId); 555 Long mailboxId = null; 556 if (accountMap != null) { 557 mailboxId = accountMap.get(type); 558 } 559 if (mailboxId == null) return Mailbox.NO_MAILBOX; 560 return mailboxId; 561 } 562 } 563 564 private void preCacheData() { 565 synchronized(mMailboxTypeMap) { 566 mMailboxTypeMap.clear(); 567 568 // Pre-cache accounts, host auth's, policies, and special mailboxes 569 preCacheTable(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null); 570 preCacheTable(HostAuth.CONTENT_URI, HostAuth.CONTENT_PROJECTION, null); 571 preCacheTable(Policy.CONTENT_URI, Policy.CONTENT_PROJECTION, null); 572 preCacheTable(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 573 MAILBOX_PRE_CACHE_SELECTION); 574 575 // Create a map from account,type to a mailbox 576 Map<String, Cursor> snapshot = mCacheMailbox.getSnapshot(); 577 Collection<Cursor> values = snapshot.values(); 578 if (values != null) { 579 for (Cursor c: values) { 580 if (c.moveToFirst()) { 581 addToMailboxTypeMap(c); 582 } 583 } 584 } 585 } 586 } 587 588 /*package*/ static SQLiteDatabase getReadableDatabase(Context context) { 589 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); 590 return helper.getReadableDatabase(); 591 } 592 593 /** 594 * Restore user Account and HostAuth data from our backup database 595 */ 596 public static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) { 597 if (Email.DEBUG) { 598 Log.w(TAG, "restoreIfNeeded..."); 599 } 600 // Check for legacy backup 601 String legacyBackup = Preferences.getLegacyBackupPreference(context); 602 // If there's a legacy backup, create a new-style backup and delete the legacy backup 603 // In the 1:1000000000 chance that the user gets an app update just as his database becomes 604 // corrupt, oh well... 605 if (!TextUtils.isEmpty(legacyBackup)) { 606 backupAccounts(context, mainDatabase); 607 Preferences.clearLegacyBackupPreference(context); 608 Log.w(TAG, "Created new EmailProvider backup database"); 609 return; 610 } 611 612 // If we have accounts, we're done 613 Cursor c = mainDatabase.query(Account.TABLE_NAME, EmailContent.ID_PROJECTION, null, null, 614 null, null, null); 615 try { 616 if (c.moveToFirst()) { 617 if (Email.DEBUG) { 618 Log.w(TAG, "restoreIfNeeded: Account exists."); 619 } 620 return; // At least one account exists. 621 } 622 } finally { 623 c.close(); 624 } 625 626 restoreAccounts(context, mainDatabase); 627 } 628 629 /** {@inheritDoc} */ 630 @Override 631 public void shutdown() { 632 if (mDatabase != null) { 633 mDatabase.close(); 634 mDatabase = null; 635 } 636 if (mBodyDatabase != null) { 637 mBodyDatabase.close(); 638 mBodyDatabase = null; 639 } 640 } 641 642 /*package*/ static void deleteMessageOrphans(SQLiteDatabase database, String tableName) { 643 if (database != null) { 644 // We'll look at all of the items in the table; there won't be many typically 645 Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); 646 // Usually, there will be nothing in these tables, so make a quick check 647 try { 648 if (c.getCount() == 0) return; 649 ArrayList<Long> foundMailboxes = new ArrayList<Long>(); 650 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>(); 651 ArrayList<Long> deleteList = new ArrayList<Long>(); 652 String[] bindArray = new String[1]; 653 while (c.moveToNext()) { 654 // Get the mailbox key and see if we've already found this mailbox 655 // If so, we're fine 656 long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY); 657 // If we already know this mailbox doesn't exist, mark the message for deletion 658 if (notFoundMailboxes.contains(mailboxId)) { 659 deleteList.add(c.getLong(ORPHANS_ID)); 660 // If we don't know about this mailbox, we'll try to find it 661 } else if (!foundMailboxes.contains(mailboxId)) { 662 bindArray[0] = Long.toString(mailboxId); 663 Cursor boxCursor = database.query(Mailbox.TABLE_NAME, 664 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null); 665 try { 666 // If it exists, we'll add it to the "found" mailboxes 667 if (boxCursor.moveToFirst()) { 668 foundMailboxes.add(mailboxId); 669 // Otherwise, we'll add to "not found" and mark the message for deletion 670 } else { 671 notFoundMailboxes.add(mailboxId); 672 deleteList.add(c.getLong(ORPHANS_ID)); 673 } 674 } finally { 675 boxCursor.close(); 676 } 677 } 678 } 679 // Now, delete the orphan messages 680 for (long messageId: deleteList) { 681 bindArray[0] = Long.toString(messageId); 682 database.delete(tableName, WHERE_ID, bindArray); 683 } 684 } finally { 685 c.close(); 686 } 687 } 688 } 689 690 @Override 691 public int delete(Uri uri, String selection, String[] selectionArgs) { 692 final int match = findMatch(uri, "delete"); 693 Context context = getContext(); 694 // Pick the correct database for this operation 695 // If we're in a transaction already (which would happen during applyBatch), then the 696 // body database is already attached to the email database and any attempt to use the 697 // body database directly will result in a SQLiteException (the database is locked) 698 SQLiteDatabase db = getDatabase(context); 699 int table = match >> BASE_SHIFT; 700 String id = "0"; 701 boolean messageDeletion = false; 702 ContentResolver resolver = context.getContentResolver(); 703 704 ContentCache cache = mContentCaches[table]; 705 String tableName = TABLE_NAMES[table]; 706 int result = -1; 707 708 try { 709 switch (match) { 710 case UI_MESSAGE: 711 // These are cases in which one or more Messages might get deleted, either by 712 // cascade or explicitly 713 case MAILBOX_ID: 714 case MAILBOX: 715 case ACCOUNT_ID: 716 case ACCOUNT: 717 case MESSAGE: 718 case SYNCED_MESSAGE_ID: 719 case MESSAGE_ID: 720 // Handle lost Body records here, since this cannot be done in a trigger 721 // The process is: 722 // 1) Begin a transaction, ensuring that both databases are affected atomically 723 // 2) Do the requested deletion, with cascading deletions handled in triggers 724 // 3) End the transaction, committing all changes atomically 725 // 726 // Bodies are auto-deleted here; Attachments are auto-deleted via trigger 727 messageDeletion = true; 728 db.beginTransaction(); 729 break; 730 } 731 switch (match) { 732 case BODY_ID: 733 case DELETED_MESSAGE_ID: 734 case SYNCED_MESSAGE_ID: 735 case MESSAGE_ID: 736 case UPDATED_MESSAGE_ID: 737 case ATTACHMENT_ID: 738 case MAILBOX_ID: 739 case ACCOUNT_ID: 740 case HOSTAUTH_ID: 741 case POLICY_ID: 742 case QUICK_RESPONSE_ID: 743 id = uri.getPathSegments().get(1); 744 if (match == SYNCED_MESSAGE_ID) { 745 // For synced messages, first copy the old message to the deleted table and 746 // delete it from the updated table (in case it was updated first) 747 // Note that this is all within a transaction, for atomicity 748 db.execSQL(DELETED_MESSAGE_INSERT + id); 749 db.execSQL(UPDATED_MESSAGE_DELETE + id); 750 } 751 if (cache != null) { 752 cache.lock(id); 753 } 754 try { 755 result = db.delete(tableName, whereWithId(id, selection), selectionArgs); 756 if (cache != null) { 757 switch(match) { 758 case ACCOUNT_ID: 759 // Account deletion will clear all of the caches, as HostAuth's, 760 // Mailboxes, and Messages will be deleted in the process 761 mCacheMailbox.invalidate("Delete", uri, selection); 762 mCacheHostAuth.invalidate("Delete", uri, selection); 763 mCachePolicy.invalidate("Delete", uri, selection); 764 //$FALL-THROUGH$ 765 case MAILBOX_ID: 766 // Mailbox deletion will clear the Message cache 767 mCacheMessage.invalidate("Delete", uri, selection); 768 //$FALL-THROUGH$ 769 case SYNCED_MESSAGE_ID: 770 case MESSAGE_ID: 771 case HOSTAUTH_ID: 772 case POLICY_ID: 773 cache.invalidate("Delete", uri, selection); 774 // Make sure all data is properly cached 775 if (match != MESSAGE_ID) { 776 preCacheData(); 777 } 778 break; 779 } 780 } 781 } finally { 782 if (cache != null) { 783 cache.unlock(id); 784 } 785 } 786 break; 787 case ATTACHMENTS_MESSAGE_ID: 788 // All attachments for the given message 789 id = uri.getPathSegments().get(2); 790 result = db.delete(tableName, 791 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs); 792 break; 793 794 case BODY: 795 case MESSAGE: 796 case DELETED_MESSAGE: 797 case UPDATED_MESSAGE: 798 case ATTACHMENT: 799 case MAILBOX: 800 case ACCOUNT: 801 case HOSTAUTH: 802 case POLICY: 803 switch(match) { 804 // See the comments above for deletion of ACCOUNT_ID, etc 805 case ACCOUNT: 806 mCacheMailbox.invalidate("Delete", uri, selection); 807 mCacheHostAuth.invalidate("Delete", uri, selection); 808 mCachePolicy.invalidate("Delete", uri, selection); 809 //$FALL-THROUGH$ 810 case MAILBOX: 811 mCacheMessage.invalidate("Delete", uri, selection); 812 //$FALL-THROUGH$ 813 case MESSAGE: 814 case HOSTAUTH: 815 case POLICY: 816 cache.invalidate("Delete", uri, selection); 817 break; 818 } 819 result = db.delete(tableName, selection, selectionArgs); 820 switch(match) { 821 case ACCOUNT: 822 case MAILBOX: 823 case HOSTAUTH: 824 case POLICY: 825 // Make sure all data is properly cached 826 preCacheData(); 827 break; 828 } 829 break; 830 831 default: 832 throw new IllegalArgumentException("Unknown URI " + uri); 833 } 834 if (messageDeletion) { 835 if (match == MESSAGE_ID) { 836 // Delete the Body record associated with the deleted message 837 db.execSQL(DELETE_BODY + id); 838 } else { 839 // Delete any orphaned Body records 840 db.execSQL(DELETE_ORPHAN_BODIES); 841 } 842 db.setTransactionSuccessful(); 843 } 844 } catch (SQLiteException e) { 845 checkDatabases(); 846 throw e; 847 } finally { 848 if (messageDeletion) { 849 db.endTransaction(); 850 } 851 } 852 853 // Notify all notifier cursors 854 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id); 855 856 // Notify all email content cursors 857 resolver.notifyChange(EmailContent.CONTENT_URI, null); 858 return result; 859 } 860 861 @Override 862 // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) 863 public String getType(Uri uri) { 864 int match = findMatch(uri, "getType"); 865 switch (match) { 866 case BODY_ID: 867 return "vnd.android.cursor.item/email-body"; 868 case BODY: 869 return "vnd.android.cursor.dir/email-body"; 870 case UPDATED_MESSAGE_ID: 871 case MESSAGE_ID: 872 // NOTE: According to the framework folks, we're supposed to invent mime types as 873 // a way of passing information to drag & drop recipients. 874 // If there's a mailboxId parameter in the url, we respond with a mime type that 875 // has -n appended, where n is the mailboxId of the message. The drag & drop code 876 // uses this information to know not to allow dragging the item to its own mailbox 877 String mimeType = EMAIL_MESSAGE_MIME_TYPE; 878 String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID); 879 if (mailboxId != null) { 880 mimeType += "-" + mailboxId; 881 } 882 return mimeType; 883 case UPDATED_MESSAGE: 884 case MESSAGE: 885 return "vnd.android.cursor.dir/email-message"; 886 case MAILBOX: 887 return "vnd.android.cursor.dir/email-mailbox"; 888 case MAILBOX_ID: 889 return "vnd.android.cursor.item/email-mailbox"; 890 case ACCOUNT: 891 return "vnd.android.cursor.dir/email-account"; 892 case ACCOUNT_ID: 893 return "vnd.android.cursor.item/email-account"; 894 case ATTACHMENTS_MESSAGE_ID: 895 case ATTACHMENT: 896 return "vnd.android.cursor.dir/email-attachment"; 897 case ATTACHMENT_ID: 898 return EMAIL_ATTACHMENT_MIME_TYPE; 899 case HOSTAUTH: 900 return "vnd.android.cursor.dir/email-hostauth"; 901 case HOSTAUTH_ID: 902 return "vnd.android.cursor.item/email-hostauth"; 903 default: 904 throw new IllegalArgumentException("Unknown URI " + uri); 905 } 906 } 907 908 @Override 909 public Uri insert(Uri uri, ContentValues values) { 910 int match = findMatch(uri, "insert"); 911 Context context = getContext(); 912 ContentResolver resolver = context.getContentResolver(); 913 914 // See the comment at delete(), above 915 SQLiteDatabase db = getDatabase(context); 916 int table = match >> BASE_SHIFT; 917 String id = "0"; 918 long longId; 919 920 // We do NOT allow setting of unreadCount/messageCount via the provider 921 // These columns are maintained via triggers 922 if (match == MAILBOX_ID || match == MAILBOX) { 923 values.put(MailboxColumns.UNREAD_COUNT, 0); 924 values.put(MailboxColumns.MESSAGE_COUNT, 0); 925 } 926 927 Uri resultUri = null; 928 929 try { 930 switch (match) { 931 // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE 932 // or DELETED_MESSAGE; see the comment below for details 933 case UPDATED_MESSAGE: 934 case DELETED_MESSAGE: 935 case MESSAGE: 936 case BODY: 937 case ATTACHMENT: 938 case MAILBOX: 939 case ACCOUNT: 940 case HOSTAUTH: 941 case POLICY: 942 case QUICK_RESPONSE: 943 longId = db.insert(TABLE_NAMES[table], "foo", values); 944 resultUri = ContentUris.withAppendedId(uri, longId); 945 switch(match) { 946 case MAILBOX: 947 if (values.containsKey(MailboxColumns.TYPE)) { 948 // Only cache special mailbox types 949 int type = values.getAsInteger(MailboxColumns.TYPE); 950 if (type != Mailbox.TYPE_INBOX && type != Mailbox.TYPE_OUTBOX && 951 type != Mailbox.TYPE_DRAFTS && type != Mailbox.TYPE_SENT && 952 type != Mailbox.TYPE_TRASH && type != Mailbox.TYPE_SEARCH) { 953 break; 954 } 955 } 956 //$FALL-THROUGH$ 957 case ACCOUNT: 958 case HOSTAUTH: 959 case POLICY: 960 // Cache new account, host auth, policy, and some mailbox rows 961 Cursor c = query(resultUri, CACHE_PROJECTIONS[table], null, null, null); 962 if (c != null) { 963 if (match == MAILBOX) { 964 addToMailboxTypeMap(c); 965 } else if (match == ACCOUNT) { 966 getOrCreateAccountMailboxTypeMap(longId); 967 } 968 c.close(); 969 } 970 break; 971 } 972 // Clients shouldn't normally be adding rows to these tables, as they are 973 // maintained by triggers. However, we need to be able to do this for unit 974 // testing, so we allow the insert and then throw the same exception that we 975 // would if this weren't allowed. 976 if (match == UPDATED_MESSAGE || match == DELETED_MESSAGE) { 977 throw new IllegalArgumentException("Unknown URL " + uri); 978 } else if (match == ATTACHMENT) { 979 int flags = 0; 980 if (values.containsKey(Attachment.FLAGS)) { 981 flags = values.getAsInteger(Attachment.FLAGS); 982 } 983 // Report all new attachments to the download service 984 mAttachmentService.attachmentChanged(getContext(), longId, flags); 985 } 986 break; 987 case MAILBOX_ID: 988 // This implies adding a message to a mailbox 989 // Hmm, a problem here is that we can't link the account as well, so it must be 990 // already in the values... 991 longId = Long.parseLong(uri.getPathSegments().get(1)); 992 values.put(MessageColumns.MAILBOX_KEY, longId); 993 return insert(Message.CONTENT_URI, values); // Recurse 994 case MESSAGE_ID: 995 // This implies adding an attachment to a message. 996 id = uri.getPathSegments().get(1); 997 longId = Long.parseLong(id); 998 values.put(AttachmentColumns.MESSAGE_KEY, longId); 999 return insert(Attachment.CONTENT_URI, values); // Recurse 1000 case ACCOUNT_ID: 1001 // This implies adding a mailbox to an account. 1002 longId = Long.parseLong(uri.getPathSegments().get(1)); 1003 values.put(MailboxColumns.ACCOUNT_KEY, longId); 1004 return insert(Mailbox.CONTENT_URI, values); // Recurse 1005 case ATTACHMENTS_MESSAGE_ID: 1006 longId = db.insert(TABLE_NAMES[table], "foo", values); 1007 resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId); 1008 break; 1009 default: 1010 throw new IllegalArgumentException("Unknown URL " + uri); 1011 } 1012 } catch (SQLiteException e) { 1013 checkDatabases(); 1014 throw e; 1015 } 1016 1017 // Notify all notifier cursors 1018 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id); 1019 1020 // Notify all existing cursors. 1021 resolver.notifyChange(EmailContent.CONTENT_URI, null); 1022 return resultUri; 1023 } 1024 1025 @Override 1026 public boolean onCreate() { 1027 Email.setServicesEnabledAsync(getContext()); 1028 checkDatabases(); 1029 return false; 1030 } 1031 1032 /** 1033 * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must 1034 * always be in sync (i.e. there are two database or NO databases). This code will delete 1035 * any "orphan" database, so that both will be created together. Note that an "orphan" database 1036 * will exist after either of the individual databases is deleted due to data corruption. 1037 */ 1038 public synchronized void checkDatabases() { 1039 // Uncache the databases 1040 if (mDatabase != null) { 1041 mDatabase = null; 1042 } 1043 if (mBodyDatabase != null) { 1044 mBodyDatabase = null; 1045 } 1046 // Look for orphans, and delete as necessary; these must always be in sync 1047 File databaseFile = getContext().getDatabasePath(DATABASE_NAME); 1048 File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME); 1049 1050 // TODO Make sure attachments are deleted 1051 if (databaseFile.exists() && !bodyFile.exists()) { 1052 Log.w(TAG, "Deleting orphaned EmailProvider database..."); 1053 databaseFile.delete(); 1054 } else if (bodyFile.exists() && !databaseFile.exists()) { 1055 Log.w(TAG, "Deleting orphaned EmailProviderBody database..."); 1056 bodyFile.delete(); 1057 } 1058 } 1059 @Override 1060 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1061 String sortOrder) { 1062 long time = 0L; 1063 if (Email.DEBUG) { 1064 time = System.nanoTime(); 1065 } 1066 Cursor c = null; 1067 int match; 1068 try { 1069 match = findMatch(uri, "query"); 1070 } catch (IllegalArgumentException e) { 1071 String uriString = uri.toString(); 1072 // If we were passed an illegal uri, see if it ends in /-1 1073 // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor 1074 if (uriString != null && uriString.endsWith("/-1")) { 1075 uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0"); 1076 match = findMatch(uri, "query"); 1077 switch (match) { 1078 case BODY_ID: 1079 case MESSAGE_ID: 1080 case DELETED_MESSAGE_ID: 1081 case UPDATED_MESSAGE_ID: 1082 case ATTACHMENT_ID: 1083 case MAILBOX_ID: 1084 case ACCOUNT_ID: 1085 case HOSTAUTH_ID: 1086 case POLICY_ID: 1087 return new MatrixCursor(projection, 0); 1088 } 1089 } 1090 throw e; 1091 } 1092 Context context = getContext(); 1093 // See the comment at delete(), above 1094 SQLiteDatabase db = getDatabase(context); 1095 int table = match >> BASE_SHIFT; 1096 String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT); 1097 String id; 1098 1099 // Find the cache for this query's table (if any) 1100 ContentCache cache = null; 1101 String tableName = TABLE_NAMES[table]; 1102 // We can only use the cache if there's no selection 1103 if (selection == null) { 1104 cache = mContentCaches[table]; 1105 } 1106 if (cache == null) { 1107 ContentCache.notCacheable(uri, selection); 1108 } 1109 1110 try { 1111 switch (match) { 1112 case MAILBOX_NOTIFICATION: 1113 c = notificationQuery(uri); 1114 return c; 1115 case MAILBOX_MOST_RECENT_MESSAGE: 1116 c = mostRecentMessageQuery(uri); 1117 return c; 1118 case ACCOUNT_DEFAULT_ID: 1119 // Start with a snapshot of the cache 1120 Map<String, Cursor> accountCache = mCacheAccount.getSnapshot(); 1121 long accountId = Account.NO_ACCOUNT; 1122 // Find the account with "isDefault" set, or the lowest account ID otherwise. 1123 // Note that the snapshot from the cached isn't guaranteed to be sorted in any 1124 // way. 1125 Collection<Cursor> accounts = accountCache.values(); 1126 for (Cursor accountCursor: accounts) { 1127 // For now, at least, we can have zero count cursors (e.g. if someone looks 1128 // up a non-existent id); we need to skip these 1129 if (accountCursor.moveToFirst()) { 1130 boolean isDefault = 1131 accountCursor.getInt(Account.CONTENT_IS_DEFAULT_COLUMN) == 1; 1132 long iterId = accountCursor.getLong(Account.CONTENT_ID_COLUMN); 1133 // We'll remember this one if it's the default or the first one we see 1134 if (isDefault) { 1135 accountId = iterId; 1136 break; 1137 } else if ((accountId == Account.NO_ACCOUNT) || (iterId < accountId)) { 1138 accountId = iterId; 1139 } 1140 } 1141 } 1142 // Return a cursor with an id projection 1143 MatrixCursor mc = new MatrixCursor(EmailContent.ID_PROJECTION); 1144 mc.addRow(new Object[] {accountId}); 1145 c = mc; 1146 break; 1147 case MAILBOX_ID_FROM_ACCOUNT_AND_TYPE: 1148 // Get accountId and type and find the mailbox in our map 1149 List<String> pathSegments = uri.getPathSegments(); 1150 accountId = Long.parseLong(pathSegments.get(1)); 1151 int type = Integer.parseInt(pathSegments.get(2)); 1152 long mailboxId = getMailboxIdFromMailboxTypeMap(accountId, type); 1153 // Return a cursor with an id projection 1154 mc = new MatrixCursor(EmailContent.ID_PROJECTION); 1155 mc.addRow(new Object[] {mailboxId}); 1156 c = mc; 1157 break; 1158 case BODY: 1159 case MESSAGE: 1160 case UPDATED_MESSAGE: 1161 case DELETED_MESSAGE: 1162 case ATTACHMENT: 1163 case MAILBOX: 1164 case ACCOUNT: 1165 case HOSTAUTH: 1166 case POLICY: 1167 case QUICK_RESPONSE: 1168 // Special-case "count of accounts"; it's common and we always know it 1169 if (match == ACCOUNT && Arrays.equals(projection, EmailContent.COUNT_COLUMNS) && 1170 selection == null && limit.equals("1")) { 1171 int accountCount = mMailboxTypeMap.size(); 1172 // In the rare case there are MAX_CACHED_ACCOUNTS or more, we can't do this 1173 if (accountCount < MAX_CACHED_ACCOUNTS) { 1174 mc = new MatrixCursor(projection, 1); 1175 mc.addRow(new Object[] {accountCount}); 1176 c = mc; 1177 break; 1178 } 1179 } 1180 c = db.query(tableName, projection, 1181 selection, selectionArgs, null, null, sortOrder, limit); 1182 break; 1183 case BODY_ID: 1184 case MESSAGE_ID: 1185 case DELETED_MESSAGE_ID: 1186 case UPDATED_MESSAGE_ID: 1187 case ATTACHMENT_ID: 1188 case MAILBOX_ID: 1189 case ACCOUNT_ID: 1190 case HOSTAUTH_ID: 1191 case POLICY_ID: 1192 case QUICK_RESPONSE_ID: 1193 id = uri.getPathSegments().get(1); 1194 if (cache != null) { 1195 c = cache.getCachedCursor(id, projection); 1196 } 1197 if (c == null) { 1198 CacheToken token = null; 1199 if (cache != null) { 1200 token = cache.getCacheToken(id); 1201 } 1202 c = db.query(tableName, projection, whereWithId(id, selection), 1203 selectionArgs, null, null, sortOrder, limit); 1204 if (cache != null) { 1205 c = cache.putCursor(c, id, projection, token); 1206 } 1207 } 1208 break; 1209 case ATTACHMENTS_MESSAGE_ID: 1210 // All attachments for the given message 1211 id = uri.getPathSegments().get(2); 1212 c = db.query(Attachment.TABLE_NAME, projection, 1213 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), 1214 selectionArgs, null, null, sortOrder, limit); 1215 break; 1216 case QUICK_RESPONSE_ACCOUNT_ID: 1217 // All quick responses for the given account 1218 id = uri.getPathSegments().get(2); 1219 c = db.query(QuickResponse.TABLE_NAME, projection, 1220 whereWith(QuickResponse.ACCOUNT_KEY + "=" + id, selection), 1221 selectionArgs, null, null, sortOrder); 1222 break; 1223 default: 1224 throw new IllegalArgumentException("Unknown URI " + uri); 1225 } 1226 } catch (SQLiteException e) { 1227 checkDatabases(); 1228 throw e; 1229 } catch (RuntimeException e) { 1230 checkDatabases(); 1231 e.printStackTrace(); 1232 throw e; 1233 } finally { 1234 if (cache != null && c != null && Email.DEBUG) { 1235 cache.recordQueryTime(c, System.nanoTime() - time); 1236 } 1237 if (c == null) { 1238 // This should never happen, but let's be sure to log it... 1239 Log.e(TAG, "Query returning null for uri: " + uri + ", selection: " + selection); 1240 } 1241 } 1242 1243 if ((c != null) && !isTemporary()) { 1244 c.setNotificationUri(getContext().getContentResolver(), uri); 1245 } 1246 return c; 1247 } 1248 1249 private String whereWithId(String id, String selection) { 1250 StringBuilder sb = new StringBuilder(256); 1251 sb.append("_id="); 1252 sb.append(id); 1253 if (selection != null) { 1254 sb.append(" AND ("); 1255 sb.append(selection); 1256 sb.append(')'); 1257 } 1258 return sb.toString(); 1259 } 1260 1261 /** 1262 * Combine a locally-generated selection with a user-provided selection 1263 * 1264 * This introduces risk that the local selection might insert incorrect chars 1265 * into the SQL, so use caution. 1266 * 1267 * @param where locally-generated selection, must not be null 1268 * @param selection user-provided selection, may be null 1269 * @return a single selection string 1270 */ 1271 private String whereWith(String where, String selection) { 1272 if (selection == null) { 1273 return where; 1274 } 1275 StringBuilder sb = new StringBuilder(where); 1276 sb.append(" AND ("); 1277 sb.append(selection); 1278 sb.append(')'); 1279 1280 return sb.toString(); 1281 } 1282 1283 /** 1284 * Restore a HostAuth from a database, given its unique id 1285 * @param db the database 1286 * @param id the unique id (_id) of the row 1287 * @return a fully populated HostAuth or null if the row does not exist 1288 */ 1289 private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) { 1290 Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION, 1291 HostAuth.RECORD_ID + "=?", new String[] {Long.toString(id)}, null, null, null); 1292 try { 1293 if (c.moveToFirst()) { 1294 HostAuth hostAuth = new HostAuth(); 1295 hostAuth.restore(c); 1296 return hostAuth; 1297 } 1298 return null; 1299 } finally { 1300 c.close(); 1301 } 1302 } 1303 1304 /** 1305 * Copy the Account and HostAuth tables from one database to another 1306 * @param fromDatabase the source database 1307 * @param toDatabase the destination database 1308 * @return the number of accounts copied, or -1 if an error occurred 1309 */ 1310 private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) { 1311 if (fromDatabase == null || toDatabase == null) return -1; 1312 1313 // Lock both databases; for the "from" database, we don't want anyone changing it from 1314 // under us; for the "to" database, we want to make the operation atomic 1315 int copyCount = 0; 1316 fromDatabase.beginTransaction(); 1317 try { 1318 toDatabase.beginTransaction(); 1319 try { 1320 // Delete anything hanging around here 1321 toDatabase.delete(Account.TABLE_NAME, null, null); 1322 toDatabase.delete(HostAuth.TABLE_NAME, null, null); 1323 1324 // Get our account cursor 1325 Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION, 1326 null, null, null, null, null); 1327 if (c == null) return 0; 1328 Log.d(TAG, "fromDatabase accounts: " + c.getCount()); 1329 try { 1330 // Loop through accounts, copying them and associated host auth's 1331 while (c.moveToNext()) { 1332 Account account = new Account(); 1333 account.restore(c); 1334 1335 // Clear security sync key and sync key, as these were specific to the 1336 // state of the account, and we've reset that... 1337 // Clear policy key so that we can re-establish policies from the server 1338 // TODO This is pretty EAS specific, but there's a lot of that around 1339 account.mSecuritySyncKey = null; 1340 account.mSyncKey = null; 1341 account.mPolicyKey = 0; 1342 1343 // Copy host auth's and update foreign keys 1344 HostAuth hostAuth = restoreHostAuth(fromDatabase, 1345 account.mHostAuthKeyRecv); 1346 1347 // The account might have gone away, though very unlikely 1348 if (hostAuth == null) continue; 1349 account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null, 1350 hostAuth.toContentValues()); 1351 1352 // EAS accounts have no send HostAuth 1353 if (account.mHostAuthKeySend > 0) { 1354 hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend); 1355 // Belt and suspenders; I can't imagine that this is possible, 1356 // since we checked the validity of the account above, and the 1357 // database is now locked 1358 if (hostAuth == null) continue; 1359 account.mHostAuthKeySend = toDatabase.insert( 1360 HostAuth.TABLE_NAME, null, hostAuth.toContentValues()); 1361 } 1362 1363 // Now, create the account in the "to" database 1364 toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues()); 1365 copyCount++; 1366 } 1367 } finally { 1368 c.close(); 1369 } 1370 1371 // Say it's ok to commit 1372 toDatabase.setTransactionSuccessful(); 1373 } finally { 1374 // STOPSHIP: Remove logging here and in at endTransaction() below 1375 Log.d(TAG, "ending toDatabase transaction; copyCount = " + copyCount); 1376 toDatabase.endTransaction(); 1377 } 1378 } catch (SQLiteException ex) { 1379 Log.w(TAG, "Exception while copying account tables", ex); 1380 copyCount = -1; 1381 } finally { 1382 Log.d(TAG, "ending fromDatabase transaction; copyCount = " + copyCount); 1383 fromDatabase.endTransaction(); 1384 } 1385 return copyCount; 1386 } 1387 1388 private static SQLiteDatabase getBackupDatabase(Context context) { 1389 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, BACKUP_DATABASE_NAME); 1390 return helper.getWritableDatabase(); 1391 } 1392 1393 /** 1394 * Backup account data, returning the number of accounts backed up 1395 */ 1396 private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) { 1397 if (Email.DEBUG) { 1398 Log.d(TAG, "backupAccounts..."); 1399 } 1400 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1401 try { 1402 int numBackedUp = copyAccountTables(mainDatabase, backupDatabase); 1403 if (numBackedUp < 0) { 1404 Log.e(TAG, "Account backup failed!"); 1405 } else if (Email.DEBUG) { 1406 Log.d(TAG, "Backed up " + numBackedUp + " accounts..."); 1407 } 1408 return numBackedUp; 1409 } finally { 1410 if (backupDatabase != null) { 1411 backupDatabase.close(); 1412 } 1413 } 1414 } 1415 1416 /** 1417 * Restore account data, returning the number of accounts restored 1418 */ 1419 private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) { 1420 if (Email.DEBUG) { 1421 Log.d(TAG, "restoreAccounts..."); 1422 } 1423 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1424 try { 1425 int numRecovered = copyAccountTables(backupDatabase, mainDatabase); 1426 if (numRecovered > 0) { 1427 Log.e(TAG, "Recovered " + numRecovered + " accounts!"); 1428 } else if (numRecovered < 0) { 1429 Log.e(TAG, "Account recovery failed?"); 1430 } else if (Email.DEBUG) { 1431 Log.d(TAG, "No accounts to restore..."); 1432 } 1433 return numRecovered; 1434 } finally { 1435 if (backupDatabase != null) { 1436 backupDatabase.close(); 1437 } 1438 } 1439 } 1440 1441 @Override 1442 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1443 // Handle this special case the fastest possible way 1444 if (uri == INTEGRITY_CHECK_URI) { 1445 checkDatabases(); 1446 return 0; 1447 } else if (uri == ACCOUNT_BACKUP_URI) { 1448 return backupAccounts(getContext(), getDatabase(getContext())); 1449 } 1450 1451 // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID) 1452 Uri notificationUri = EmailContent.CONTENT_URI; 1453 1454 int match = findMatch(uri, "update"); 1455 Context context = getContext(); 1456 ContentResolver resolver = context.getContentResolver(); 1457 // See the comment at delete(), above 1458 SQLiteDatabase db = getDatabase(context); 1459 int table = match >> BASE_SHIFT; 1460 int result; 1461 1462 // We do NOT allow setting of unreadCount/messageCount via the provider 1463 // These columns are maintained via triggers 1464 if (match == MAILBOX_ID || match == MAILBOX) { 1465 values.remove(MailboxColumns.UNREAD_COUNT); 1466 values.remove(MailboxColumns.MESSAGE_COUNT); 1467 } 1468 1469 ContentCache cache = mContentCaches[table]; 1470 String tableName = TABLE_NAMES[table]; 1471 String id = "0"; 1472 1473 try { 1474 outer: 1475 switch (match) { 1476 case MAILBOX_ID_ADD_TO_FIELD: 1477 case ACCOUNT_ID_ADD_TO_FIELD: 1478 id = uri.getPathSegments().get(1); 1479 String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME); 1480 Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME); 1481 if (field == null || add == null) { 1482 throw new IllegalArgumentException("No field/add specified " + uri); 1483 } 1484 ContentValues actualValues = new ContentValues(); 1485 if (cache != null) { 1486 cache.lock(id); 1487 } 1488 try { 1489 db.beginTransaction(); 1490 try { 1491 Cursor c = db.query(tableName, 1492 new String[] {EmailContent.RECORD_ID, field}, 1493 whereWithId(id, selection), 1494 selectionArgs, null, null, null); 1495 try { 1496 result = 0; 1497 String[] bind = new String[1]; 1498 if (c.moveToNext()) { 1499 bind[0] = c.getString(0); // _id 1500 long value = c.getLong(1) + add; 1501 actualValues.put(field, value); 1502 result = db.update(tableName, actualValues, ID_EQUALS, bind); 1503 } 1504 db.setTransactionSuccessful(); 1505 } finally { 1506 c.close(); 1507 } 1508 } finally { 1509 db.endTransaction(); 1510 } 1511 } finally { 1512 if (cache != null) { 1513 cache.unlock(id, actualValues); 1514 } 1515 } 1516 break; 1517 case SYNCED_MESSAGE_ID: 1518 case UPDATED_MESSAGE_ID: 1519 case MESSAGE_ID: 1520 case BODY_ID: 1521 case ATTACHMENT_ID: 1522 case MAILBOX_ID: 1523 case ACCOUNT_ID: 1524 case HOSTAUTH_ID: 1525 case QUICK_RESPONSE_ID: 1526 case POLICY_ID: 1527 id = uri.getPathSegments().get(1); 1528 if (cache != null) { 1529 cache.lock(id); 1530 } 1531 try { 1532 if (match == SYNCED_MESSAGE_ID) { 1533 // For synced messages, first copy the old message to the updated table 1534 // Note the insert or ignore semantics, guaranteeing that only the first 1535 // update will be reflected in the updated message table; therefore this 1536 // row will always have the "original" data 1537 db.execSQL(UPDATED_MESSAGE_INSERT + id); 1538 } else if (match == MESSAGE_ID) { 1539 db.execSQL(UPDATED_MESSAGE_DELETE + id); 1540 } 1541 result = db.update(tableName, values, whereWithId(id, selection), 1542 selectionArgs); 1543 } catch (SQLiteException e) { 1544 // Null out values (so they aren't cached) and re-throw 1545 values = null; 1546 throw e; 1547 } finally { 1548 if (cache != null) { 1549 cache.unlock(id, values); 1550 } 1551 } 1552 if (match == ATTACHMENT_ID) { 1553 long attId = Integer.parseInt(id); 1554 if (values.containsKey(Attachment.FLAGS)) { 1555 int flags = values.getAsInteger(Attachment.FLAGS); 1556 mAttachmentService.attachmentChanged(context, attId, flags); 1557 } 1558 } 1559 break; 1560 case BODY: 1561 case MESSAGE: 1562 case UPDATED_MESSAGE: 1563 case ATTACHMENT: 1564 case MAILBOX: 1565 case ACCOUNT: 1566 case HOSTAUTH: 1567 case POLICY: 1568 switch(match) { 1569 // To avoid invalidating the cache on updates, we execute them one at a 1570 // time using the XXX_ID uri; these are all executed atomically 1571 case ACCOUNT: 1572 case MAILBOX: 1573 case HOSTAUTH: 1574 case POLICY: 1575 Cursor c = db.query(tableName, EmailContent.ID_PROJECTION, 1576 selection, selectionArgs, null, null, null); 1577 db.beginTransaction(); 1578 result = 0; 1579 try { 1580 while (c.moveToNext()) { 1581 update(ContentUris.withAppendedId( 1582 uri, c.getLong(EmailContent.ID_PROJECTION_COLUMN)), 1583 values, null, null); 1584 result++; 1585 } 1586 db.setTransactionSuccessful(); 1587 } finally { 1588 db.endTransaction(); 1589 c.close(); 1590 } 1591 break outer; 1592 // Any cached table other than those above should be invalidated here 1593 case MESSAGE: 1594 // If we're doing some generic update, the whole cache needs to be 1595 // invalidated. This case should be quite rare 1596 cache.invalidate("Update", uri, selection); 1597 //$FALL-THROUGH$ 1598 default: 1599 result = db.update(tableName, values, selection, selectionArgs); 1600 break outer; 1601 } 1602 case ACCOUNT_RESET_NEW_COUNT_ID: 1603 id = uri.getPathSegments().get(1); 1604 if (cache != null) { 1605 cache.lock(id); 1606 } 1607 ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 1608 if (values != null) { 1609 Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME); 1610 if (set != null) { 1611 newMessageCount = new ContentValues(); 1612 newMessageCount.put(Account.NEW_MESSAGE_COUNT, set); 1613 } 1614 } 1615 try { 1616 result = db.update(tableName, newMessageCount, 1617 whereWithId(id, selection), selectionArgs); 1618 } finally { 1619 if (cache != null) { 1620 cache.unlock(id, values); 1621 } 1622 } 1623 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1624 break; 1625 case ACCOUNT_RESET_NEW_COUNT: 1626 result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT, 1627 selection, selectionArgs); 1628 // Affects all accounts. Just invalidate all account cache. 1629 cache.invalidate("Reset all new counts", null, null); 1630 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1631 break; 1632 default: 1633 throw new IllegalArgumentException("Unknown URI " + uri); 1634 } 1635 } catch (SQLiteException e) { 1636 checkDatabases(); 1637 throw e; 1638 } 1639 1640 // Notify all notifier cursors 1641 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id); 1642 1643 resolver.notifyChange(notificationUri, null); 1644 return result; 1645 } 1646 1647 /** 1648 * Returns the base notification URI for the given content type. 1649 * 1650 * @param match The type of content that was modified. 1651 */ 1652 private Uri getBaseNotificationUri(int match) { 1653 Uri baseUri = null; 1654 switch (match) { 1655 case MESSAGE: 1656 case MESSAGE_ID: 1657 case SYNCED_MESSAGE_ID: 1658 baseUri = Message.NOTIFIER_URI; 1659 break; 1660 case ACCOUNT: 1661 case ACCOUNT_ID: 1662 baseUri = Account.NOTIFIER_URI; 1663 break; 1664 } 1665 return baseUri; 1666 } 1667 1668 /** 1669 * Sends a change notification to any cursors observers of the given base URI. The final 1670 * notification URI is dynamically built to contain the specified information. It will be 1671 * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending 1672 * upon the given values. For message-related notifications, we also notify the widget 1673 * provider. 1674 * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked. 1675 * If this is necessary, it can be added. However, due to the implementation of 1676 * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications. 1677 * 1678 * @param baseUri The base URI to send notifications to. Must be able to take appended IDs. 1679 * @param op Optional operation to be appended to the URI. 1680 * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be 1681 * appended to the base URI. 1682 */ 1683 private void sendNotifierChange(Uri baseUri, String op, String id) { 1684 if (baseUri == null) return; 1685 Uri uri = baseUri; 1686 1687 // Append the operation, if specified 1688 if (op != null) { 1689 uri = baseUri.buildUpon().appendEncodedPath(op).build(); 1690 } 1691 1692 long longId = 0L; 1693 try { 1694 longId = Long.valueOf(id); 1695 } catch (NumberFormatException ignore) {} 1696 1697 final ContentResolver resolver = getContext().getContentResolver(); 1698 if (longId > 0) { 1699 resolver.notifyChange(ContentUris.withAppendedId(uri, longId), null); 1700 } else { 1701 resolver.notifyChange(uri, null); 1702 } 1703 1704 // If a message has changed, notify any widgets 1705 if (baseUri.equals(Message.NOTIFIER_URI)) { 1706 sendMessageListDataChangedNotification(); 1707 } 1708 } 1709 1710 private void sendMessageListDataChangedNotification() { 1711 final Context context = getContext(); 1712 final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED); 1713 // Ideally this intent would contain information about which account changed, to limit the 1714 // updates to that particular account. Unfortunately, that information is not available in 1715 // sendNotifierChange(). 1716 context.sendBroadcast(intent); 1717 } 1718 1719 @Override 1720 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1721 throws OperationApplicationException { 1722 Context context = getContext(); 1723 SQLiteDatabase db = getDatabase(context); 1724 db.beginTransaction(); 1725 try { 1726 ContentProviderResult[] results = super.applyBatch(operations); 1727 db.setTransactionSuccessful(); 1728 return results; 1729 } finally { 1730 db.endTransaction(); 1731 } 1732 } 1733 1734 /** 1735 * For testing purposes, check whether a given row is cached 1736 * @param baseUri the base uri of the EmailContent 1737 * @param id the row id of the EmailContent 1738 * @return whether or not the row is currently cached 1739 */ 1740 @VisibleForTesting 1741 protected boolean isCached(Uri baseUri, long id) { 1742 int match = findMatch(baseUri, "isCached"); 1743 int table = match >> BASE_SHIFT; 1744 ContentCache cache = mContentCaches[table]; 1745 if (cache == null) return false; 1746 Cursor cc = cache.get(Long.toString(id)); 1747 return (cc != null); 1748 } 1749 1750 public static interface AttachmentService { 1751 /** 1752 * Notify the service that an attachment has changed. 1753 */ 1754 void attachmentChanged(Context context, long id, int flags); 1755 } 1756 1757 private final AttachmentService DEFAULT_ATTACHMENT_SERVICE = new AttachmentService() { 1758 @Override 1759 public void attachmentChanged(Context context, long id, int flags) { 1760 // The default implementation delegates to the real service. 1761 AttachmentDownloadService.attachmentChanged(context, id, flags); 1762 } 1763 }; 1764 private AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE; 1765 1766 /** 1767 * Injects a custom attachment service handler. If null is specified, will reset to the 1768 * default service. 1769 */ 1770 public void injectAttachmentService(AttachmentService as) { 1771 mAttachmentService = (as == null) ? DEFAULT_ATTACHMENT_SERVICE : as; 1772 } 1773 1774 // SELECT DISTINCT Boxes._id, Boxes.unreadCount count(Message._id) from Message, 1775 // (SELECT _id, unreadCount, messageCount, lastNotifiedMessageCount, lastNotifiedMessageKey 1776 // FROM Mailbox WHERE accountKey=6 AND ((type = 0) OR (syncInterval!=0 AND syncInterval!=-1))) 1777 // AS Boxes 1778 // WHERE Boxes.messageCount!=Boxes.lastNotifiedMessageCount 1779 // OR (Boxes._id=Message.mailboxKey AND Message._id>Boxes.lastNotifiedMessageKey) 1780 // AND flagRead = 0 AND timeStamp != 0 1781 // TODO: This query can be simplified a bit 1782 private static final String NOTIFICATION_QUERY = 1783 "SELECT DISTINCT Boxes." + MailboxColumns.ID + ", Boxes." + MailboxColumns.UNREAD_COUNT + 1784 ", count(" + Message.TABLE_NAME + "." + MessageColumns.ID + ")" + 1785 " FROM " + 1786 Message.TABLE_NAME + "," + 1787 "(SELECT " + MailboxColumns.ID + "," + MailboxColumns.UNREAD_COUNT + "," + 1788 MailboxColumns.MESSAGE_COUNT + "," + MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT + 1789 "," + MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY + " FROM " + Mailbox.TABLE_NAME + 1790 " WHERE " + MailboxColumns.ACCOUNT_KEY + "=?" + 1791 " AND (" + MailboxColumns.TYPE + "=" + Mailbox.TYPE_INBOX + " OR (" 1792 + MailboxColumns.SYNC_INTERVAL + "!=0 AND " + 1793 MailboxColumns.SYNC_INTERVAL + "!=-1))) AS Boxes " + 1794 "WHERE Boxes." + MailboxColumns.ID + '=' + Message.TABLE_NAME + "." + 1795 MessageColumns.MAILBOX_KEY + " AND " + Message.TABLE_NAME + "." + 1796 MessageColumns.ID + ">Boxes." + MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY + 1797 " AND " + MessageColumns.FLAG_READ + "=0 AND " + MessageColumns.TIMESTAMP + "!=0"; 1798 1799 public Cursor notificationQuery(Uri uri) { 1800 SQLiteDatabase db = getDatabase(getContext()); 1801 String accountId = uri.getLastPathSegment(); 1802 return db.rawQuery(NOTIFICATION_QUERY, new String[] {accountId}); 1803 } 1804 1805 public Cursor mostRecentMessageQuery(Uri uri) { 1806 SQLiteDatabase db = getDatabase(getContext()); 1807 String mailboxId = uri.getLastPathSegment(); 1808 return db.rawQuery("select max(_id) from Message where mailboxKey=?", 1809 new String[] {mailboxId}); 1810 } 1811 } 1812