EmailProvider.java revision cc0185f07c9198008d8dc685ae9979f3e35e8539
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.appwidget.AppWidgetManager; 20import android.content.ComponentName; 21import android.content.ContentProvider; 22import android.content.ContentProviderOperation; 23import android.content.ContentProviderResult; 24import android.content.ContentResolver; 25import android.content.ContentUris; 26import android.content.ContentValues; 27import android.content.Context; 28import android.content.Intent; 29import android.content.OperationApplicationException; 30import android.content.UriMatcher; 31import android.database.ContentObserver; 32import android.database.Cursor; 33import android.database.CursorWrapper; 34import android.database.MatrixCursor; 35import android.database.MergeCursor; 36import android.database.sqlite.SQLiteDatabase; 37import android.database.sqlite.SQLiteException; 38import android.net.Uri; 39import android.os.Bundle; 40import android.os.Parcel; 41import android.os.RemoteException; 42import android.provider.BaseColumns; 43import android.text.TextUtils; 44import android.util.Log; 45 46import com.android.common.content.ProjectionMap; 47import com.android.email.NotificationController; 48import com.android.email.Preferences; 49import com.android.email.R; 50import com.android.email.SecurityPolicy; 51import com.android.email.provider.ContentCache.CacheToken; 52import com.android.email.service.AttachmentDownloadService; 53import com.android.email.service.EmailServiceUtils; 54import com.android.email.service.EmailServiceUtils.EmailServiceInfo; 55import com.android.email2.ui.MailActivityEmail; 56import com.android.emailcommon.Logging; 57import com.android.emailcommon.mail.Address; 58import com.android.emailcommon.provider.Account; 59import com.android.emailcommon.provider.EmailContent; 60import com.android.emailcommon.provider.EmailContent.AccountColumns; 61import com.android.emailcommon.provider.EmailContent.Attachment; 62import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 63import com.android.emailcommon.provider.EmailContent.Body; 64import com.android.emailcommon.provider.EmailContent.BodyColumns; 65import com.android.emailcommon.provider.EmailContent.MailboxColumns; 66import com.android.emailcommon.provider.EmailContent.Message; 67import com.android.emailcommon.provider.EmailContent.MessageColumns; 68import com.android.emailcommon.provider.EmailContent.PolicyColumns; 69import com.android.emailcommon.provider.EmailContent.SyncColumns; 70import com.android.emailcommon.provider.HostAuth; 71import com.android.emailcommon.provider.Mailbox; 72import com.android.emailcommon.provider.Policy; 73import com.android.emailcommon.provider.QuickResponse; 74import com.android.emailcommon.service.EmailServiceProxy; 75import com.android.emailcommon.service.IEmailService; 76import com.android.emailcommon.service.IEmailServiceCallback; 77import com.android.emailcommon.service.SearchParams; 78import com.android.emailcommon.utility.AttachmentUtilities; 79import com.android.emailcommon.utility.Utility; 80import com.android.mail.providers.UIProvider; 81import com.android.mail.providers.UIProvider.AccountCapabilities; 82import com.android.mail.providers.UIProvider.AccountCursorExtraKeys; 83import com.android.mail.providers.UIProvider.ConversationPriority; 84import com.android.mail.providers.UIProvider.ConversationSendingState; 85import com.android.mail.providers.UIProvider.DraftType; 86import com.android.mail.utils.LogUtils; 87import com.android.mail.utils.MatrixCursorWithExtra; 88import com.android.mail.utils.Utils; 89import com.android.mail.widget.BaseWidgetProvider; 90import com.android.mail.widget.WidgetProvider; 91import com.google.common.annotations.VisibleForTesting; 92 93import java.io.File; 94import java.util.ArrayList; 95import java.util.Arrays; 96import java.util.Collection; 97import java.util.HashMap; 98import java.util.List; 99import java.util.Map; 100import java.util.regex.Pattern; 101 102/** 103 * @author mblank 104 * 105 */ 106public class EmailProvider extends ContentProvider { 107 108 private static final String TAG = "EmailProvider"; 109 110 public static final String EMAIL_APP_MIME_TYPE = "application/email-ls"; 111 112 protected static final String DATABASE_NAME = "EmailProvider.db"; 113 protected static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; 114 protected static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db"; 115 116 public static final String ACTION_ATTACHMENT_UPDATED = "com.android.email.ATTACHMENT_UPDATED"; 117 public static final String ATTACHMENT_UPDATED_EXTRA_FLAGS = 118 "com.android.email.ATTACHMENT_UPDATED_FLAGS"; 119 120 /** 121 * Notifies that changes happened. Certain UI components, e.g., widgets, can register for this 122 * {@link android.content.Intent} and update accordingly. However, this can be very broad and 123 * is NOT the preferred way of getting notification. 124 */ 125 public static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED = 126 "com.android.email.MESSAGE_LIST_DATASET_CHANGED"; 127 128 public static final String EMAIL_MESSAGE_MIME_TYPE = 129 "vnd.android.cursor.item/email-message"; 130 public static final String EMAIL_ATTACHMENT_MIME_TYPE = 131 "vnd.android.cursor.item/email-attachment"; 132 133 public static final Uri INTEGRITY_CHECK_URI = 134 Uri.parse("content://" + EmailContent.AUTHORITY + "/integrityCheck"); 135 public static final Uri ACCOUNT_BACKUP_URI = 136 Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup"); 137 public static final Uri FOLDER_STATUS_URI = 138 Uri.parse("content://" + EmailContent.AUTHORITY + "/status"); 139 public static final Uri FOLDER_REFRESH_URI = 140 Uri.parse("content://" + EmailContent.AUTHORITY + "/refresh"); 141 142 /** Appended to the notification URI for delete operations */ 143 public static final String NOTIFICATION_OP_DELETE = "delete"; 144 /** Appended to the notification URI for insert operations */ 145 public static final String NOTIFICATION_OP_INSERT = "insert"; 146 /** Appended to the notification URI for update operations */ 147 public static final String NOTIFICATION_OP_UPDATE = "update"; 148 149 // Definitions for our queries looking for orphaned messages 150 private static final String[] ORPHANS_PROJECTION 151 = new String[] {MessageColumns.ID, MessageColumns.MAILBOX_KEY}; 152 private static final int ORPHANS_ID = 0; 153 private static final int ORPHANS_MAILBOX_KEY = 1; 154 155 private static final String WHERE_ID = EmailContent.RECORD_ID + "=?"; 156 157 // This is not a hard limit on accounts, per se, but beyond this, we can't guarantee that all 158 // critical mailboxes, host auth's, accounts, and policies are cached 159 private static final int MAX_CACHED_ACCOUNTS = 16; 160 // Inbox, Drafts, Sent, Outbox, Trash, and Search (these boxes are cached when possible) 161 private static final int NUM_ALWAYS_CACHED_MAILBOXES = 6; 162 163 // We'll cache the following four tables; sizes are best estimates of effective values 164 private final ContentCache mCacheAccount = 165 new ContentCache("Account", Account.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS); 166 private final ContentCache mCacheHostAuth = 167 new ContentCache("HostAuth", HostAuth.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS * 2); 168 /*package*/ final ContentCache mCacheMailbox = 169 new ContentCache("Mailbox", Mailbox.CONTENT_PROJECTION, 170 MAX_CACHED_ACCOUNTS * (NUM_ALWAYS_CACHED_MAILBOXES + 2)); 171 private final ContentCache mCacheMessage = 172 new ContentCache("Message", Message.CONTENT_PROJECTION, 8); 173 private final ContentCache mCachePolicy = 174 new ContentCache("Policy", Policy.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS); 175 176 private static final int ACCOUNT_BASE = 0; 177 private static final int ACCOUNT = ACCOUNT_BASE; 178 private static final int ACCOUNT_ID = ACCOUNT_BASE + 1; 179 private static final int ACCOUNT_ID_ADD_TO_FIELD = ACCOUNT_BASE + 2; 180 private static final int ACCOUNT_RESET_NEW_COUNT = ACCOUNT_BASE + 3; 181 private static final int ACCOUNT_RESET_NEW_COUNT_ID = ACCOUNT_BASE + 4; 182 private static final int ACCOUNT_DEFAULT_ID = ACCOUNT_BASE + 5; 183 private static final int ACCOUNT_CHECK = ACCOUNT_BASE + 6; 184 185 private static final int MAILBOX_BASE = 0x1000; 186 private static final int MAILBOX = MAILBOX_BASE; 187 private static final int MAILBOX_ID = MAILBOX_BASE + 1; 188 private static final int MAILBOX_ID_FROM_ACCOUNT_AND_TYPE = MAILBOX_BASE + 2; 189 private static final int MAILBOX_ID_ADD_TO_FIELD = MAILBOX_BASE + 3; 190 private static final int MAILBOX_NOTIFICATION = MAILBOX_BASE + 4; 191 private static final int MAILBOX_MOST_RECENT_MESSAGE = MAILBOX_BASE + 5; 192 193 private static final int MESSAGE_BASE = 0x2000; 194 private static final int MESSAGE = MESSAGE_BASE; 195 private static final int MESSAGE_ID = MESSAGE_BASE + 1; 196 private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2; 197 198 private static final int ATTACHMENT_BASE = 0x3000; 199 private static final int ATTACHMENT = ATTACHMENT_BASE; 200 private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1; 201 private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2; 202 203 private static final int HOSTAUTH_BASE = 0x4000; 204 private static final int HOSTAUTH = HOSTAUTH_BASE; 205 private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1; 206 207 private static final int UPDATED_MESSAGE_BASE = 0x5000; 208 private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE; 209 private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1; 210 211 private static final int DELETED_MESSAGE_BASE = 0x6000; 212 private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE; 213 private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1; 214 215 private static final int POLICY_BASE = 0x7000; 216 private static final int POLICY = POLICY_BASE; 217 private static final int POLICY_ID = POLICY_BASE + 1; 218 219 private static final int QUICK_RESPONSE_BASE = 0x8000; 220 private static final int QUICK_RESPONSE = QUICK_RESPONSE_BASE; 221 private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1; 222 private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2; 223 224 private static final int UI_BASE = 0x9000; 225 private static final int UI_FOLDERS = UI_BASE; 226 private static final int UI_SUBFOLDERS = UI_BASE + 1; 227 private static final int UI_MESSAGES = UI_BASE + 2; 228 private static final int UI_MESSAGE = UI_BASE + 3; 229 private static final int UI_SENDMAIL = UI_BASE + 4; 230 private static final int UI_UNDO = UI_BASE + 5; 231 private static final int UI_SAVEDRAFT = UI_BASE + 6; 232 private static final int UI_UPDATEDRAFT = UI_BASE + 7; 233 private static final int UI_SENDDRAFT = UI_BASE + 8; 234 private static final int UI_FOLDER_REFRESH = UI_BASE + 9; 235 private static final int UI_FOLDER = UI_BASE + 10; 236 private static final int UI_ACCOUNT = UI_BASE + 11; 237 private static final int UI_ACCTS = UI_BASE + 12; 238 private static final int UI_ATTACHMENTS = UI_BASE + 13; 239 private static final int UI_ATTACHMENT = UI_BASE + 14; 240 private static final int UI_SEARCH = UI_BASE + 15; 241 private static final int UI_ACCOUNT_DATA = UI_BASE + 16; 242 private static final int UI_FOLDER_LOAD_MORE = UI_BASE + 17; 243 private static final int UI_CONVERSATION = UI_BASE + 18; 244 private static final int UI_RECENT_FOLDERS = UI_BASE + 19; 245 private static final int UI_DEFAULT_RECENT_FOLDERS = UI_BASE + 20; 246 private static final int UI_ALL_FOLDERS = UI_BASE + 21; 247 248 // MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS 249 private static final int LAST_EMAIL_PROVIDER_DB_BASE = UI_BASE; 250 251 // DO NOT CHANGE BODY_BASE!! 252 private static final int BODY_BASE = LAST_EMAIL_PROVIDER_DB_BASE + 0x1000; 253 private static final int BODY = BODY_BASE; 254 private static final int BODY_ID = BODY_BASE + 1; 255 256 private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc. 257 258 // TABLE_NAMES MUST remain in the order of the BASE constants above (e.g. ACCOUNT_BASE = 0x0000, 259 // MESSAGE_BASE = 0x1000, etc.) 260 private static final String[] TABLE_NAMES = { 261 Account.TABLE_NAME, 262 Mailbox.TABLE_NAME, 263 Message.TABLE_NAME, 264 Attachment.TABLE_NAME, 265 HostAuth.TABLE_NAME, 266 Message.UPDATED_TABLE_NAME, 267 Message.DELETED_TABLE_NAME, 268 Policy.TABLE_NAME, 269 QuickResponse.TABLE_NAME, 270 null, // UI 271 Body.TABLE_NAME, 272 }; 273 274 // CONTENT_CACHES MUST remain in the order of the BASE constants above 275 private final ContentCache[] mContentCaches = { 276 mCacheAccount, 277 mCacheMailbox, 278 mCacheMessage, 279 null, // Attachment 280 mCacheHostAuth, 281 null, // Updated message 282 null, // Deleted message 283 mCachePolicy, 284 null, // Quick response 285 null, // Body 286 null // UI 287 }; 288 289 // CACHE_PROJECTIONS MUST remain in the order of the BASE constants above 290 private static final String[][] CACHE_PROJECTIONS = { 291 Account.CONTENT_PROJECTION, 292 Mailbox.CONTENT_PROJECTION, 293 Message.CONTENT_PROJECTION, 294 null, // Attachment 295 HostAuth.CONTENT_PROJECTION, 296 null, // Updated message 297 null, // Deleted message 298 Policy.CONTENT_PROJECTION, 299 null, // Quick response 300 null, // Body 301 null // UI 302 }; 303 304 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 305 306 private static final String MAILBOX_PRE_CACHE_SELECTION = MailboxColumns.TYPE + " IN (" + 307 Mailbox.TYPE_INBOX + "," + Mailbox.TYPE_DRAFTS + "," + Mailbox.TYPE_TRASH + "," + 308 Mailbox.TYPE_SENT + "," + Mailbox.TYPE_SEARCH + "," + Mailbox.TYPE_OUTBOX + ")"; 309 310 /** 311 * Let's only generate these SQL strings once, as they are used frequently 312 * Note that this isn't relevant for table creation strings, since they are used only once 313 */ 314 private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " + 315 Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 316 EmailContent.RECORD_ID + '='; 317 318 private static final String UPDATED_MESSAGE_DELETE = "delete from " + 319 Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '='; 320 321 private static final String DELETED_MESSAGE_INSERT = "insert or replace into " + 322 Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 323 EmailContent.RECORD_ID + '='; 324 325 private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME + 326 " where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY + 327 " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " + 328 Message.TABLE_NAME + ')'; 329 330 private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME + 331 " where " + BodyColumns.MESSAGE_KEY + '='; 332 333 private static final String ID_EQUALS = EmailContent.RECORD_ID + "=?"; 334 335 private static final ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 336 private static final ContentValues EMPTY_CONTENT_VALUES = new ContentValues(); 337 338 public static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; 339 340 // For undo handling 341 private int mLastSequence = -1; 342 private ArrayList<ContentProviderOperation> mLastSequenceOps = 343 new ArrayList<ContentProviderOperation>(); 344 345 // Query parameter indicating the command came from UIProvider 346 private static final String IS_UIPROVIDER = "is_uiprovider"; 347 348 static { 349 // Email URI matching table 350 UriMatcher matcher = sURIMatcher; 351 352 // All accounts 353 matcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT); 354 // A specific account 355 // insert into this URI causes a mailbox to be added to the account 356 matcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID); 357 matcher.addURI(EmailContent.AUTHORITY, "account/default", ACCOUNT_DEFAULT_ID); 358 matcher.addURI(EmailContent.AUTHORITY, "accountCheck/#", ACCOUNT_CHECK); 359 360 // Special URI to reset the new message count. Only update works, and content values 361 // will be ignored. 362 matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount", 363 ACCOUNT_RESET_NEW_COUNT); 364 matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount/#", 365 ACCOUNT_RESET_NEW_COUNT_ID); 366 367 // All mailboxes 368 matcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX); 369 // A specific mailbox 370 // insert into this URI causes a message to be added to the mailbox 371 // ** NOTE For now, the accountKey must be set manually in the values! 372 matcher.addURI(EmailContent.AUTHORITY, "mailbox/#", MAILBOX_ID); 373 matcher.addURI(EmailContent.AUTHORITY, "mailboxIdFromAccountAndType/#/#", 374 MAILBOX_ID_FROM_ACCOUNT_AND_TYPE); 375 matcher.addURI(EmailContent.AUTHORITY, "mailboxNotification/#", MAILBOX_NOTIFICATION); 376 matcher.addURI(EmailContent.AUTHORITY, "mailboxMostRecentMessage/#", 377 MAILBOX_MOST_RECENT_MESSAGE); 378 379 // All messages 380 matcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE); 381 // A specific message 382 // insert into this URI causes an attachment to be added to the message 383 matcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID); 384 385 // A specific attachment 386 matcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT); 387 // A specific attachment (the header information) 388 matcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID); 389 // The attachments of a specific message (query only) (insert & delete TBD) 390 matcher.addURI(EmailContent.AUTHORITY, "attachment/message/#", 391 ATTACHMENTS_MESSAGE_ID); 392 393 // All mail bodies 394 matcher.addURI(EmailContent.AUTHORITY, "body", BODY); 395 // A specific mail body 396 matcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID); 397 398 // All hostauth records 399 matcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH); 400 // A specific hostauth 401 matcher.addURI(EmailContent.AUTHORITY, "hostauth/#", HOSTAUTH_ID); 402 403 // Atomically a constant value to a particular field of a mailbox/account 404 matcher.addURI(EmailContent.AUTHORITY, "mailboxIdAddToField/#", 405 MAILBOX_ID_ADD_TO_FIELD); 406 matcher.addURI(EmailContent.AUTHORITY, "accountIdAddToField/#", 407 ACCOUNT_ID_ADD_TO_FIELD); 408 409 /** 410 * THIS URI HAS SPECIAL SEMANTICS 411 * ITS USE IS INTENDED FOR THE UI APPLICATION TO MARK CHANGES THAT NEED TO BE SYNCED BACK 412 * TO A SERVER VIA A SYNC ADAPTER 413 */ 414 matcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); 415 416 /** 417 * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY 418 * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI 419 * BY THE UI APPLICATION 420 */ 421 // All deleted messages 422 matcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE); 423 // A specific deleted message 424 matcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID); 425 426 // All updated messages 427 matcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE); 428 // A specific updated message 429 matcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID); 430 431 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues(); 432 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0); 433 434 matcher.addURI(EmailContent.AUTHORITY, "policy", POLICY); 435 matcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID); 436 437 // All quick responses 438 matcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE); 439 // A specific quick response 440 matcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID); 441 // All quick responses associated with a particular account id 442 matcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#", 443 QUICK_RESPONSE_ACCOUNT_ID); 444 445 matcher.addURI(EmailContent.AUTHORITY, "uifolders/#", UI_FOLDERS); 446 matcher.addURI(EmailContent.AUTHORITY, "uiallfolders/#", UI_ALL_FOLDERS); 447 matcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS); 448 matcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES); 449 matcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE); 450 matcher.addURI(EmailContent.AUTHORITY, "uisendmail/#", UI_SENDMAIL); 451 matcher.addURI(EmailContent.AUTHORITY, "uiundo", UI_UNDO); 452 matcher.addURI(EmailContent.AUTHORITY, "uisavedraft/#", UI_SAVEDRAFT); 453 matcher.addURI(EmailContent.AUTHORITY, "uiupdatedraft/#", UI_UPDATEDRAFT); 454 matcher.addURI(EmailContent.AUTHORITY, "uisenddraft/#", UI_SENDDRAFT); 455 matcher.addURI(EmailContent.AUTHORITY, "uirefresh/#", UI_FOLDER_REFRESH); 456 matcher.addURI(EmailContent.AUTHORITY, "uifolder/#", UI_FOLDER); 457 matcher.addURI(EmailContent.AUTHORITY, "uiaccount/#", UI_ACCOUNT); 458 matcher.addURI(EmailContent.AUTHORITY, "uiaccts", UI_ACCTS); 459 matcher.addURI(EmailContent.AUTHORITY, "uiattachments/#", UI_ATTACHMENTS); 460 matcher.addURI(EmailContent.AUTHORITY, "uiattachment/#", UI_ATTACHMENT); 461 matcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH); 462 matcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA); 463 matcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE); 464 matcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION); 465 matcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS); 466 matcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#", 467 UI_DEFAULT_RECENT_FOLDERS); 468 } 469 470 /** 471 * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in 472 * @param uri the Uri to match 473 * @return the match value 474 */ 475 private static int findMatch(Uri uri, String methodName) { 476 int match = sURIMatcher.match(uri); 477 if (match < 0) { 478 throw new IllegalArgumentException("Unknown uri: " + uri); 479 } else if (Logging.LOGD) { 480 Log.v(TAG, methodName + ": uri=" + uri + ", match is " + match); 481 } 482 return match; 483 } 484 485 private SQLiteDatabase mDatabase; 486 private SQLiteDatabase mBodyDatabase; 487 488 public static Uri uiUri(String type, long id) { 489 return Uri.parse(uiUriString(type, id)); 490 } 491 492 /** 493 * Creates a URI string from a database ID (guaranteed to be unique). 494 * @param type of the resource: uifolder, message, etc. 495 * @param id the id of the resource. 496 * @return 497 */ 498 public static String uiUriString(String type, long id) { 499 return "content://" + EmailContent.AUTHORITY + "/" + type + ((id == -1) ? "" : ("/" + id)); 500 } 501 502 /** 503 * Orphan record deletion utility. Generates a sqlite statement like: 504 * delete from <table> where <column> not in (select <foreignColumn> from <foreignTable>) 505 * @param db the EmailProvider database 506 * @param table the table whose orphans are to be removed 507 * @param column the column deletion will be based on 508 * @param foreignColumn the column in the foreign table whose absence will trigger the deletion 509 * @param foreignTable the foreign table 510 */ 511 @VisibleForTesting 512 void deleteUnlinked(SQLiteDatabase db, String table, String column, String foreignColumn, 513 String foreignTable) { 514 int count = db.delete(table, column + " not in (select " + foreignColumn + " from " + 515 foreignTable + ")", null); 516 if (count > 0) { 517 Log.w(TAG, "Found " + count + " orphaned row(s) in " + table); 518 } 519 } 520 521 @VisibleForTesting 522 synchronized SQLiteDatabase getDatabase(Context context) { 523 // Always return the cached database, if we've got one 524 if (mDatabase != null) { 525 return mDatabase; 526 } 527 528 // Whenever we create or re-cache the databases, make sure that we haven't lost one 529 // to corruption 530 checkDatabases(); 531 532 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); 533 mDatabase = helper.getWritableDatabase(); 534 DBHelper.BodyDatabaseHelper bodyHelper = 535 new DBHelper.BodyDatabaseHelper(context, BODY_DATABASE_NAME); 536 mBodyDatabase = bodyHelper.getWritableDatabase(); 537 if (mBodyDatabase != null) { 538 String bodyFileName = mBodyDatabase.getPath(); 539 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); 540 } 541 542 // Restore accounts if the database is corrupted... 543 restoreIfNeeded(context, mDatabase); 544 // Check for any orphaned Messages in the updated/deleted tables 545 deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME); 546 deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME); 547 // Delete orphaned mailboxes/messages/policies (account no longer exists) 548 deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY, AccountColumns.ID, 549 Account.TABLE_NAME); 550 deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY, AccountColumns.ID, 551 Account.TABLE_NAME); 552 deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns.ID, AccountColumns.POLICY_KEY, 553 Account.TABLE_NAME); 554 initUiProvider(); 555 preCacheData(); 556 return mDatabase; 557 } 558 559 /** 560 * Perform startup actions related to UI 561 */ 562 private void initUiProvider() { 563 // Clear mailbox sync status 564 mDatabase.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UI_SYNC_STATUS + 565 "=" + UIProvider.SyncStatus.NO_SYNC); 566 } 567 568 /** 569 * Pre-cache all of the items in a given table meeting the selection criteria 570 * @param tableUri the table uri 571 * @param baseProjection the base projection of that table 572 * @param selection the selection criteria 573 */ 574 private void preCacheTable(Uri tableUri, String[] baseProjection, String selection) { 575 Cursor c = query(tableUri, EmailContent.ID_PROJECTION, selection, null, null); 576 try { 577 while (c.moveToNext()) { 578 long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 579 Cursor cachedCursor = query(ContentUris.withAppendedId( 580 tableUri, id), baseProjection, null, null, null); 581 if (cachedCursor != null) { 582 // For accounts, create a mailbox type map entry (if necessary) 583 if (tableUri == Account.CONTENT_URI) { 584 getOrCreateAccountMailboxTypeMap(id); 585 } 586 cachedCursor.close(); 587 } 588 } 589 } finally { 590 c.close(); 591 } 592 } 593 594 private final HashMap<Long, HashMap<Integer, Long>> mMailboxTypeMap = 595 new HashMap<Long, HashMap<Integer, Long>>(); 596 597 private HashMap<Integer, Long> getOrCreateAccountMailboxTypeMap(long accountId) { 598 synchronized(mMailboxTypeMap) { 599 HashMap<Integer, Long> accountMailboxTypeMap = mMailboxTypeMap.get(accountId); 600 if (accountMailboxTypeMap == null) { 601 accountMailboxTypeMap = new HashMap<Integer, Long>(); 602 mMailboxTypeMap.put(accountId, accountMailboxTypeMap); 603 } 604 return accountMailboxTypeMap; 605 } 606 } 607 608 private void addToMailboxTypeMap(Cursor c) { 609 long accountId = c.getLong(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN); 610 int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 611 synchronized(mMailboxTypeMap) { 612 HashMap<Integer, Long> accountMailboxTypeMap = 613 getOrCreateAccountMailboxTypeMap(accountId); 614 accountMailboxTypeMap.put(type, c.getLong(Mailbox.CONTENT_ID_COLUMN)); 615 } 616 } 617 618 private long getMailboxIdFromMailboxTypeMap(long accountId, int type) { 619 synchronized(mMailboxTypeMap) { 620 HashMap<Integer, Long> accountMap = mMailboxTypeMap.get(accountId); 621 Long mailboxId = null; 622 if (accountMap != null) { 623 mailboxId = accountMap.get(type); 624 } 625 if (mailboxId == null) return Mailbox.NO_MAILBOX; 626 return mailboxId; 627 } 628 } 629 630 private void preCacheData() { 631 synchronized(mMailboxTypeMap) { 632 mMailboxTypeMap.clear(); 633 634 // Pre-cache accounts, host auth's, policies, and special mailboxes 635 preCacheTable(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null); 636 preCacheTable(HostAuth.CONTENT_URI, HostAuth.CONTENT_PROJECTION, null); 637 preCacheTable(Policy.CONTENT_URI, Policy.CONTENT_PROJECTION, null); 638 preCacheTable(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 639 MAILBOX_PRE_CACHE_SELECTION); 640 641 // Create a map from account,type to a mailbox 642 Map<String, Cursor> snapshot = mCacheMailbox.getSnapshot(); 643 Collection<Cursor> values = snapshot.values(); 644 if (values != null) { 645 for (Cursor c: values) { 646 if (c.moveToFirst()) { 647 addToMailboxTypeMap(c); 648 } 649 } 650 } 651 } 652 } 653 654 /*package*/ static SQLiteDatabase getReadableDatabase(Context context) { 655 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); 656 return helper.getReadableDatabase(); 657 } 658 659 /** 660 * Restore user Account and HostAuth data from our backup database 661 */ 662 public static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) { 663 if (MailActivityEmail.DEBUG) { 664 Log.w(TAG, "restoreIfNeeded..."); 665 } 666 // Check for legacy backup 667 String legacyBackup = Preferences.getLegacyBackupPreference(context); 668 // If there's a legacy backup, create a new-style backup and delete the legacy backup 669 // In the 1:1000000000 chance that the user gets an app update just as his database becomes 670 // corrupt, oh well... 671 if (!TextUtils.isEmpty(legacyBackup)) { 672 backupAccounts(context, mainDatabase); 673 Preferences.clearLegacyBackupPreference(context); 674 Log.w(TAG, "Created new EmailProvider backup database"); 675 return; 676 } 677 678 // If we have accounts, we're done 679 Cursor c = mainDatabase.query(Account.TABLE_NAME, EmailContent.ID_PROJECTION, null, null, 680 null, null, null); 681 try { 682 if (c.moveToFirst()) { 683 if (MailActivityEmail.DEBUG) { 684 Log.w(TAG, "restoreIfNeeded: Account exists."); 685 } 686 return; // At least one account exists. 687 } 688 } finally { 689 c.close(); 690 } 691 692 restoreAccounts(context, mainDatabase); 693 } 694 695 /** {@inheritDoc} */ 696 @Override 697 public void shutdown() { 698 if (mDatabase != null) { 699 mDatabase.close(); 700 mDatabase = null; 701 } 702 if (mBodyDatabase != null) { 703 mBodyDatabase.close(); 704 mBodyDatabase = null; 705 } 706 } 707 708 /*package*/ static void deleteMessageOrphans(SQLiteDatabase database, String tableName) { 709 if (database != null) { 710 // We'll look at all of the items in the table; there won't be many typically 711 Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); 712 // Usually, there will be nothing in these tables, so make a quick check 713 try { 714 if (c.getCount() == 0) return; 715 ArrayList<Long> foundMailboxes = new ArrayList<Long>(); 716 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>(); 717 ArrayList<Long> deleteList = new ArrayList<Long>(); 718 String[] bindArray = new String[1]; 719 while (c.moveToNext()) { 720 // Get the mailbox key and see if we've already found this mailbox 721 // If so, we're fine 722 long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY); 723 // If we already know this mailbox doesn't exist, mark the message for deletion 724 if (notFoundMailboxes.contains(mailboxId)) { 725 deleteList.add(c.getLong(ORPHANS_ID)); 726 // If we don't know about this mailbox, we'll try to find it 727 } else if (!foundMailboxes.contains(mailboxId)) { 728 bindArray[0] = Long.toString(mailboxId); 729 Cursor boxCursor = database.query(Mailbox.TABLE_NAME, 730 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null); 731 try { 732 // If it exists, we'll add it to the "found" mailboxes 733 if (boxCursor.moveToFirst()) { 734 foundMailboxes.add(mailboxId); 735 // Otherwise, we'll add to "not found" and mark the message for deletion 736 } else { 737 notFoundMailboxes.add(mailboxId); 738 deleteList.add(c.getLong(ORPHANS_ID)); 739 } 740 } finally { 741 boxCursor.close(); 742 } 743 } 744 } 745 // Now, delete the orphan messages 746 for (long messageId: deleteList) { 747 bindArray[0] = Long.toString(messageId); 748 database.delete(tableName, WHERE_ID, bindArray); 749 } 750 } finally { 751 c.close(); 752 } 753 } 754 } 755 756 @Override 757 public int delete(Uri uri, String selection, String[] selectionArgs) { 758 final int match = findMatch(uri, "delete"); 759 Context context = getContext(); 760 // Pick the correct database for this operation 761 // If we're in a transaction already (which would happen during applyBatch), then the 762 // body database is already attached to the email database and any attempt to use the 763 // body database directly will result in a SQLiteException (the database is locked) 764 SQLiteDatabase db = getDatabase(context); 765 int table = match >> BASE_SHIFT; 766 String id = "0"; 767 boolean messageDeletion = false; 768 ContentResolver resolver = context.getContentResolver(); 769 770 ContentCache cache = mContentCaches[table]; 771 String tableName = TABLE_NAMES[table]; 772 int result = -1; 773 774 try { 775 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 776 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 777 notifyUIConversation(uri); 778 } 779 } 780 switch (match) { 781 case UI_MESSAGE: 782 return uiDeleteMessage(uri); 783 case UI_ACCOUNT_DATA: 784 return uiDeleteAccountData(uri); 785 case UI_ACCOUNT: 786 return uiDeleteAccount(uri); 787 // These are cases in which one or more Messages might get deleted, either by 788 // cascade or explicitly 789 case MAILBOX_ID: 790 case MAILBOX: 791 case ACCOUNT_ID: 792 case ACCOUNT: 793 case MESSAGE: 794 case SYNCED_MESSAGE_ID: 795 case MESSAGE_ID: 796 // Handle lost Body records here, since this cannot be done in a trigger 797 // The process is: 798 // 1) Begin a transaction, ensuring that both databases are affected atomically 799 // 2) Do the requested deletion, with cascading deletions handled in triggers 800 // 3) End the transaction, committing all changes atomically 801 // 802 // Bodies are auto-deleted here; Attachments are auto-deleted via trigger 803 messageDeletion = true; 804 db.beginTransaction(); 805 break; 806 } 807 switch (match) { 808 case BODY_ID: 809 case DELETED_MESSAGE_ID: 810 case SYNCED_MESSAGE_ID: 811 case MESSAGE_ID: 812 case UPDATED_MESSAGE_ID: 813 case ATTACHMENT_ID: 814 case MAILBOX_ID: 815 case ACCOUNT_ID: 816 case HOSTAUTH_ID: 817 case POLICY_ID: 818 case QUICK_RESPONSE_ID: 819 id = uri.getPathSegments().get(1); 820 if (match == SYNCED_MESSAGE_ID) { 821 // For synced messages, first copy the old message to the deleted table and 822 // delete it from the updated table (in case it was updated first) 823 // Note that this is all within a transaction, for atomicity 824 db.execSQL(DELETED_MESSAGE_INSERT + id); 825 db.execSQL(UPDATED_MESSAGE_DELETE + id); 826 } 827 if (cache != null) { 828 cache.lock(id); 829 } 830 try { 831 result = db.delete(tableName, whereWithId(id, selection), selectionArgs); 832 if (cache != null) { 833 switch(match) { 834 case ACCOUNT_ID: 835 // Account deletion will clear all of the caches, as HostAuth's, 836 // Mailboxes, and Messages will be deleted in the process 837 mCacheMailbox.invalidate("Delete", uri, selection); 838 mCacheHostAuth.invalidate("Delete", uri, selection); 839 mCachePolicy.invalidate("Delete", uri, selection); 840 //$FALL-THROUGH$ 841 case MAILBOX_ID: 842 // Mailbox deletion will clear the Message cache 843 mCacheMessage.invalidate("Delete", uri, selection); 844 //$FALL-THROUGH$ 845 case SYNCED_MESSAGE_ID: 846 case MESSAGE_ID: 847 case HOSTAUTH_ID: 848 case POLICY_ID: 849 cache.invalidate("Delete", uri, selection); 850 // Make sure all data is properly cached 851 if (match != MESSAGE_ID) { 852 preCacheData(); 853 } 854 break; 855 } 856 } 857 } finally { 858 if (cache != null) { 859 cache.unlock(id); 860 } 861 } 862 if (match == ACCOUNT_ID) { 863 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id); 864 resolver.notifyChange(UIPROVIDER_ACCOUNTS_NOTIFIER, null); 865 } else if (match == MAILBOX_ID) { 866 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, id); 867 } 868 break; 869 case ATTACHMENTS_MESSAGE_ID: 870 // All attachments for the given message 871 id = uri.getPathSegments().get(2); 872 result = db.delete(tableName, 873 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs); 874 break; 875 876 case BODY: 877 case MESSAGE: 878 case DELETED_MESSAGE: 879 case UPDATED_MESSAGE: 880 case ATTACHMENT: 881 case MAILBOX: 882 case ACCOUNT: 883 case HOSTAUTH: 884 case POLICY: 885 switch(match) { 886 // See the comments above for deletion of ACCOUNT_ID, etc 887 case ACCOUNT: 888 mCacheMailbox.invalidate("Delete", uri, selection); 889 mCacheHostAuth.invalidate("Delete", uri, selection); 890 mCachePolicy.invalidate("Delete", uri, selection); 891 //$FALL-THROUGH$ 892 case MAILBOX: 893 mCacheMessage.invalidate("Delete", uri, selection); 894 //$FALL-THROUGH$ 895 case MESSAGE: 896 case HOSTAUTH: 897 case POLICY: 898 cache.invalidate("Delete", uri, selection); 899 break; 900 } 901 result = db.delete(tableName, selection, selectionArgs); 902 switch(match) { 903 case ACCOUNT: 904 case MAILBOX: 905 case HOSTAUTH: 906 case POLICY: 907 // Make sure all data is properly cached 908 preCacheData(); 909 break; 910 } 911 break; 912 913 default: 914 throw new IllegalArgumentException("Unknown URI " + uri); 915 } 916 if (messageDeletion) { 917 if (match == MESSAGE_ID) { 918 // Delete the Body record associated with the deleted message 919 db.execSQL(DELETE_BODY + id); 920 } else { 921 // Delete any orphaned Body records 922 db.execSQL(DELETE_ORPHAN_BODIES); 923 } 924 db.setTransactionSuccessful(); 925 } 926 } catch (SQLiteException e) { 927 checkDatabases(); 928 throw e; 929 } finally { 930 if (messageDeletion) { 931 db.endTransaction(); 932 } 933 } 934 935 // Notify all notifier cursors 936 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id); 937 938 // Notify all email content cursors 939 resolver.notifyChange(EmailContent.CONTENT_URI, null); 940 return result; 941 } 942 943 @Override 944 // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) 945 public String getType(Uri uri) { 946 int match = findMatch(uri, "getType"); 947 switch (match) { 948 case BODY_ID: 949 return "vnd.android.cursor.item/email-body"; 950 case BODY: 951 return "vnd.android.cursor.dir/email-body"; 952 case UPDATED_MESSAGE_ID: 953 case MESSAGE_ID: 954 // NOTE: According to the framework folks, we're supposed to invent mime types as 955 // a way of passing information to drag & drop recipients. 956 // If there's a mailboxId parameter in the url, we respond with a mime type that 957 // has -n appended, where n is the mailboxId of the message. The drag & drop code 958 // uses this information to know not to allow dragging the item to its own mailbox 959 String mimeType = EMAIL_MESSAGE_MIME_TYPE; 960 String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID); 961 if (mailboxId != null) { 962 mimeType += "-" + mailboxId; 963 } 964 return mimeType; 965 case UPDATED_MESSAGE: 966 case MESSAGE: 967 return "vnd.android.cursor.dir/email-message"; 968 case MAILBOX: 969 return "vnd.android.cursor.dir/email-mailbox"; 970 case MAILBOX_ID: 971 return "vnd.android.cursor.item/email-mailbox"; 972 case ACCOUNT: 973 return "vnd.android.cursor.dir/email-account"; 974 case ACCOUNT_ID: 975 return "vnd.android.cursor.item/email-account"; 976 case ATTACHMENTS_MESSAGE_ID: 977 case ATTACHMENT: 978 return "vnd.android.cursor.dir/email-attachment"; 979 case ATTACHMENT_ID: 980 return EMAIL_ATTACHMENT_MIME_TYPE; 981 case HOSTAUTH: 982 return "vnd.android.cursor.dir/email-hostauth"; 983 case HOSTAUTH_ID: 984 return "vnd.android.cursor.item/email-hostauth"; 985 default: 986 throw new IllegalArgumentException("Unknown URI " + uri); 987 } 988 } 989 990 private static final Uri UIPROVIDER_CONVERSATION_NOTIFIER = 991 Uri.parse("content://" + UIProvider.AUTHORITY + "/uimessages"); 992 private static final Uri UIPROVIDER_FOLDER_NOTIFIER = 993 Uri.parse("content://" + UIProvider.AUTHORITY + "/uifolder"); 994 private static final Uri UIPROVIDER_ACCOUNT_NOTIFIER = 995 Uri.parse("content://" + UIProvider.AUTHORITY + "/uiaccount"); 996 public static final Uri UIPROVIDER_SETTINGS_NOTIFIER = 997 Uri.parse("content://" + UIProvider.AUTHORITY + "/uisettings"); 998 private static final Uri UIPROVIDER_ATTACHMENT_NOTIFIER = 999 Uri.parse("content://" + UIProvider.AUTHORITY + "/uiattachment"); 1000 private static final Uri UIPROVIDER_ATTACHMENTS_NOTIFIER = 1001 Uri.parse("content://" + UIProvider.AUTHORITY + "/uiattachments"); 1002 private static final Uri UIPROVIDER_ACCOUNTS_NOTIFIER = 1003 Uri.parse("content://" + UIProvider.AUTHORITY + "/uiaccts"); 1004 private static final Uri UIPROVIDER_MESSAGE_NOTIFIER = 1005 Uri.parse("content://" + UIProvider.AUTHORITY + "/uimessage"); 1006 private static final Uri UIPROVIDER_RECENT_FOLDERS_NOTIFIER = 1007 Uri.parse("content://" + UIProvider.AUTHORITY + "/uirecentfolders"); 1008 1009 @Override 1010 public Uri insert(Uri uri, ContentValues values) { 1011 int match = findMatch(uri, "insert"); 1012 Context context = getContext(); 1013 ContentResolver resolver = context.getContentResolver(); 1014 1015 // See the comment at delete(), above 1016 SQLiteDatabase db = getDatabase(context); 1017 int table = match >> BASE_SHIFT; 1018 String id = "0"; 1019 long longId; 1020 1021 // We do NOT allow setting of unreadCount/messageCount via the provider 1022 // These columns are maintained via triggers 1023 if (match == MAILBOX_ID || match == MAILBOX) { 1024 values.put(MailboxColumns.UNREAD_COUNT, 0); 1025 values.put(MailboxColumns.MESSAGE_COUNT, 0); 1026 } 1027 1028 Uri resultUri = null; 1029 1030 try { 1031 switch (match) { 1032 case UI_SAVEDRAFT: 1033 return uiSaveDraft(uri, values); 1034 case UI_SENDMAIL: 1035 return uiSendMail(uri, values); 1036 // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE 1037 // or DELETED_MESSAGE; see the comment below for details 1038 case UPDATED_MESSAGE: 1039 case DELETED_MESSAGE: 1040 case MESSAGE: 1041 case BODY: 1042 case ATTACHMENT: 1043 case MAILBOX: 1044 case ACCOUNT: 1045 case HOSTAUTH: 1046 case POLICY: 1047 case QUICK_RESPONSE: 1048 longId = db.insert(TABLE_NAMES[table], "foo", values); 1049 resultUri = ContentUris.withAppendedId(uri, longId); 1050 switch(match) { 1051 case MESSAGE: 1052 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 1053 notifyUIConversationMailbox(values.getAsLong(Message.MAILBOX_KEY)); 1054 } 1055 break; 1056 case MAILBOX: 1057 if (values.containsKey(MailboxColumns.TYPE)) { 1058 // Only cache special mailbox types 1059 int type = values.getAsInteger(MailboxColumns.TYPE); 1060 if (type != Mailbox.TYPE_INBOX && type != Mailbox.TYPE_OUTBOX && 1061 type != Mailbox.TYPE_DRAFTS && type != Mailbox.TYPE_SENT && 1062 type != Mailbox.TYPE_TRASH && type != Mailbox.TYPE_SEARCH) { 1063 break; 1064 } 1065 } 1066 // Notify the account when a new mailbox is added 1067 Long accountId = values.getAsLong(MailboxColumns.ACCOUNT_KEY); 1068 if (accountId != null && accountId.longValue() > 0) { 1069 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, accountId); 1070 } 1071 //$FALL-THROUGH$ 1072 case ACCOUNT: 1073 case HOSTAUTH: 1074 case POLICY: 1075 // Cache new account, host auth, policy, and some mailbox rows 1076 Cursor c = query(resultUri, CACHE_PROJECTIONS[table], null, null, null); 1077 if (c != null) { 1078 if (match == MAILBOX) { 1079 addToMailboxTypeMap(c); 1080 } else if (match == ACCOUNT) { 1081 getOrCreateAccountMailboxTypeMap(longId); 1082 } 1083 c.close(); 1084 } 1085 break; 1086 } 1087 // Clients shouldn't normally be adding rows to these tables, as they are 1088 // maintained by triggers. However, we need to be able to do this for unit 1089 // testing, so we allow the insert and then throw the same exception that we 1090 // would if this weren't allowed. 1091 if (match == UPDATED_MESSAGE || match == DELETED_MESSAGE) { 1092 throw new IllegalArgumentException("Unknown URL " + uri); 1093 } else if (match == ATTACHMENT) { 1094 int flags = 0; 1095 if (values.containsKey(Attachment.FLAGS)) { 1096 flags = values.getAsInteger(Attachment.FLAGS); 1097 } 1098 // Report all new attachments to the download service 1099 mAttachmentService.attachmentChanged(getContext(), longId, flags); 1100 } else if (match == ACCOUNT) { 1101 resolver.notifyChange(UIPROVIDER_ACCOUNTS_NOTIFIER, null); 1102 } 1103 break; 1104 case MAILBOX_ID: 1105 // This implies adding a message to a mailbox 1106 // Hmm, a problem here is that we can't link the account as well, so it must be 1107 // already in the values... 1108 longId = Long.parseLong(uri.getPathSegments().get(1)); 1109 values.put(MessageColumns.MAILBOX_KEY, longId); 1110 return insert(Message.CONTENT_URI, values); // Recurse 1111 case MESSAGE_ID: 1112 // This implies adding an attachment to a message. 1113 id = uri.getPathSegments().get(1); 1114 longId = Long.parseLong(id); 1115 values.put(AttachmentColumns.MESSAGE_KEY, longId); 1116 return insert(Attachment.CONTENT_URI, values); // Recurse 1117 case ACCOUNT_ID: 1118 // This implies adding a mailbox to an account. 1119 longId = Long.parseLong(uri.getPathSegments().get(1)); 1120 values.put(MailboxColumns.ACCOUNT_KEY, longId); 1121 return insert(Mailbox.CONTENT_URI, values); // Recurse 1122 case ATTACHMENTS_MESSAGE_ID: 1123 longId = db.insert(TABLE_NAMES[table], "foo", values); 1124 resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId); 1125 break; 1126 default: 1127 throw new IllegalArgumentException("Unknown URL " + uri); 1128 } 1129 } catch (SQLiteException e) { 1130 checkDatabases(); 1131 throw e; 1132 } 1133 1134 // Notify all notifier cursors 1135 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id); 1136 1137 // Notify all existing cursors. 1138 resolver.notifyChange(EmailContent.CONTENT_URI, null); 1139 return resultUri; 1140 } 1141 1142 @Override 1143 public boolean onCreate() { 1144 MailActivityEmail.setServicesEnabledAsync(getContext()); 1145 checkDatabases(); 1146 return false; 1147 } 1148 1149 /** 1150 * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must 1151 * always be in sync (i.e. there are two database or NO databases). This code will delete 1152 * any "orphan" database, so that both will be created together. Note that an "orphan" database 1153 * will exist after either of the individual databases is deleted due to data corruption. 1154 */ 1155 public void checkDatabases() { 1156 // Uncache the databases 1157 if (mDatabase != null) { 1158 mDatabase = null; 1159 } 1160 if (mBodyDatabase != null) { 1161 mBodyDatabase = null; 1162 } 1163 // Look for orphans, and delete as necessary; these must always be in sync 1164 File databaseFile = getContext().getDatabasePath(DATABASE_NAME); 1165 File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME); 1166 1167 // TODO Make sure attachments are deleted 1168 if (databaseFile.exists() && !bodyFile.exists()) { 1169 Log.w(TAG, "Deleting orphaned EmailProvider database..."); 1170 databaseFile.delete(); 1171 } else if (bodyFile.exists() && !databaseFile.exists()) { 1172 Log.w(TAG, "Deleting orphaned EmailProviderBody database..."); 1173 bodyFile.delete(); 1174 } 1175 } 1176 @Override 1177 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1178 String sortOrder) { 1179 long time = 0L; 1180 if (MailActivityEmail.DEBUG) { 1181 time = System.nanoTime(); 1182 } 1183 Cursor c = null; 1184 int match; 1185 try { 1186 match = findMatch(uri, "query"); 1187 } catch (IllegalArgumentException e) { 1188 String uriString = uri.toString(); 1189 // If we were passed an illegal uri, see if it ends in /-1 1190 // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor 1191 if (uriString != null && uriString.endsWith("/-1")) { 1192 uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0"); 1193 match = findMatch(uri, "query"); 1194 switch (match) { 1195 case BODY_ID: 1196 case MESSAGE_ID: 1197 case DELETED_MESSAGE_ID: 1198 case UPDATED_MESSAGE_ID: 1199 case ATTACHMENT_ID: 1200 case MAILBOX_ID: 1201 case ACCOUNT_ID: 1202 case HOSTAUTH_ID: 1203 case POLICY_ID: 1204 return new MatrixCursor(projection, 0); 1205 } 1206 } 1207 throw e; 1208 } 1209 Context context = getContext(); 1210 // See the comment at delete(), above 1211 SQLiteDatabase db = getDatabase(context); 1212 int table = match >> BASE_SHIFT; 1213 String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT); 1214 String id; 1215 1216 // Find the cache for this query's table (if any) 1217 ContentCache cache = null; 1218 String tableName = TABLE_NAMES[table]; 1219 // We can only use the cache if there's no selection 1220 if (selection == null) { 1221 cache = mContentCaches[table]; 1222 } 1223 if (cache == null) { 1224 ContentCache.notCacheable(uri, selection); 1225 } 1226 1227 try { 1228 switch (match) { 1229 // First, dispatch queries from UnfiedEmail 1230 case UI_SEARCH: 1231 return uiSearch(uri, projection); 1232 case UI_ACCTS: 1233 c = uiAccounts(projection); 1234 return c; 1235 case UI_UNDO: 1236 return uiUndo(projection); 1237 case UI_SUBFOLDERS: 1238 case UI_MESSAGES: 1239 case UI_MESSAGE: 1240 case UI_FOLDER: 1241 case UI_ACCOUNT: 1242 case UI_ATTACHMENT: 1243 case UI_ATTACHMENTS: 1244 case UI_CONVERSATION: 1245 case UI_RECENT_FOLDERS: 1246 case UI_ALL_FOLDERS: 1247 // For now, we don't allow selection criteria within these queries 1248 if (selection != null || selectionArgs != null) { 1249 throw new IllegalArgumentException("UI queries can't have selection/args"); 1250 } 1251 c = uiQuery(match, uri, projection); 1252 return c; 1253 case UI_FOLDERS: 1254 c = uiFolders(uri, projection); 1255 return c; 1256 case UI_FOLDER_LOAD_MORE: 1257 c = uiFolderLoadMore(uri); 1258 return c; 1259 case UI_FOLDER_REFRESH: 1260 c = uiFolderRefresh(uri); 1261 return c; 1262 case MAILBOX_NOTIFICATION: 1263 c = notificationQuery(uri); 1264 return c; 1265 case MAILBOX_MOST_RECENT_MESSAGE: 1266 c = mostRecentMessageQuery(uri); 1267 return c; 1268 case ACCOUNT_DEFAULT_ID: 1269 // Start with a snapshot of the cache 1270 Map<String, Cursor> accountCache = mCacheAccount.getSnapshot(); 1271 long accountId = Account.NO_ACCOUNT; 1272 // Find the account with "isDefault" set, or the lowest account ID otherwise. 1273 // Note that the snapshot from the cached isn't guaranteed to be sorted in any 1274 // way. 1275 Collection<Cursor> accounts = accountCache.values(); 1276 for (Cursor accountCursor: accounts) { 1277 // For now, at least, we can have zero count cursors (e.g. if someone looks 1278 // up a non-existent id); we need to skip these 1279 if (accountCursor.moveToFirst()) { 1280 boolean isDefault = 1281 accountCursor.getInt(Account.CONTENT_IS_DEFAULT_COLUMN) == 1; 1282 long iterId = accountCursor.getLong(Account.CONTENT_ID_COLUMN); 1283 // We'll remember this one if it's the default or the first one we see 1284 if (isDefault) { 1285 accountId = iterId; 1286 break; 1287 } else if ((accountId == Account.NO_ACCOUNT) || (iterId < accountId)) { 1288 accountId = iterId; 1289 } 1290 } 1291 } 1292 // Return a cursor with an id projection 1293 MatrixCursor mc = new MatrixCursor(EmailContent.ID_PROJECTION); 1294 mc.addRow(new Object[] {accountId}); 1295 c = mc; 1296 break; 1297 case MAILBOX_ID_FROM_ACCOUNT_AND_TYPE: 1298 // Get accountId and type and find the mailbox in our map 1299 List<String> pathSegments = uri.getPathSegments(); 1300 accountId = Long.parseLong(pathSegments.get(1)); 1301 int type = Integer.parseInt(pathSegments.get(2)); 1302 long mailboxId = getMailboxIdFromMailboxTypeMap(accountId, type); 1303 // Return a cursor with an id projection 1304 mc = new MatrixCursor(EmailContent.ID_PROJECTION); 1305 mc.addRow(new Object[] {mailboxId}); 1306 c = mc; 1307 break; 1308 case BODY: 1309 case MESSAGE: 1310 case UPDATED_MESSAGE: 1311 case DELETED_MESSAGE: 1312 case ATTACHMENT: 1313 case MAILBOX: 1314 case ACCOUNT: 1315 case HOSTAUTH: 1316 case POLICY: 1317 case QUICK_RESPONSE: 1318 // Special-case "count of accounts"; it's common and we always know it 1319 if (match == ACCOUNT && Arrays.equals(projection, EmailContent.COUNT_COLUMNS) && 1320 selection == null && limit.equals("1")) { 1321 int accountCount = mMailboxTypeMap.size(); 1322 // In the rare case there are MAX_CACHED_ACCOUNTS or more, we can't do this 1323 if (accountCount < MAX_CACHED_ACCOUNTS) { 1324 mc = new MatrixCursor(projection, 1); 1325 mc.addRow(new Object[] {accountCount}); 1326 c = mc; 1327 break; 1328 } 1329 } 1330 c = db.query(tableName, projection, 1331 selection, selectionArgs, null, null, sortOrder, limit); 1332 break; 1333 case BODY_ID: 1334 case MESSAGE_ID: 1335 case DELETED_MESSAGE_ID: 1336 case UPDATED_MESSAGE_ID: 1337 case ATTACHMENT_ID: 1338 case MAILBOX_ID: 1339 case ACCOUNT_ID: 1340 case HOSTAUTH_ID: 1341 case POLICY_ID: 1342 case QUICK_RESPONSE_ID: 1343 id = uri.getPathSegments().get(1); 1344 if (cache != null) { 1345 c = cache.getCachedCursor(id, projection); 1346 } 1347 if (c == null) { 1348 CacheToken token = null; 1349 if (cache != null) { 1350 token = cache.getCacheToken(id); 1351 } 1352 c = db.query(tableName, projection, whereWithId(id, selection), 1353 selectionArgs, null, null, sortOrder, limit); 1354 if (cache != null) { 1355 c = cache.putCursor(c, id, projection, token); 1356 } 1357 } 1358 break; 1359 case ATTACHMENTS_MESSAGE_ID: 1360 // All attachments for the given message 1361 id = uri.getPathSegments().get(2); 1362 c = db.query(Attachment.TABLE_NAME, projection, 1363 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), 1364 selectionArgs, null, null, sortOrder, limit); 1365 break; 1366 case QUICK_RESPONSE_ACCOUNT_ID: 1367 // All quick responses for the given account 1368 id = uri.getPathSegments().get(2); 1369 c = db.query(QuickResponse.TABLE_NAME, projection, 1370 whereWith(QuickResponse.ACCOUNT_KEY + "=" + id, selection), 1371 selectionArgs, null, null, sortOrder); 1372 break; 1373 default: 1374 throw new IllegalArgumentException("Unknown URI " + uri); 1375 } 1376 } catch (SQLiteException e) { 1377 checkDatabases(); 1378 throw e; 1379 } catch (RuntimeException e) { 1380 checkDatabases(); 1381 e.printStackTrace(); 1382 throw e; 1383 } finally { 1384 if (cache != null && c != null && MailActivityEmail.DEBUG) { 1385 cache.recordQueryTime(c, System.nanoTime() - time); 1386 } 1387 if (c == null) { 1388 // This should never happen, but let's be sure to log it... 1389 Log.e(TAG, "Query returning null for uri: " + uri + ", selection: " + selection); 1390 } 1391 } 1392 1393 if ((c != null) && !isTemporary()) { 1394 c.setNotificationUri(getContext().getContentResolver(), uri); 1395 } 1396 return c; 1397 } 1398 1399 private String whereWithId(String id, String selection) { 1400 StringBuilder sb = new StringBuilder(256); 1401 sb.append("_id="); 1402 sb.append(id); 1403 if (selection != null) { 1404 sb.append(" AND ("); 1405 sb.append(selection); 1406 sb.append(')'); 1407 } 1408 return sb.toString(); 1409 } 1410 1411 /** 1412 * Combine a locally-generated selection with a user-provided selection 1413 * 1414 * This introduces risk that the local selection might insert incorrect chars 1415 * into the SQL, so use caution. 1416 * 1417 * @param where locally-generated selection, must not be null 1418 * @param selection user-provided selection, may be null 1419 * @return a single selection string 1420 */ 1421 private String whereWith(String where, String selection) { 1422 if (selection == null) { 1423 return where; 1424 } 1425 StringBuilder sb = new StringBuilder(where); 1426 sb.append(" AND ("); 1427 sb.append(selection); 1428 sb.append(')'); 1429 1430 return sb.toString(); 1431 } 1432 1433 /** 1434 * Restore a HostAuth from a database, given its unique id 1435 * @param db the database 1436 * @param id the unique id (_id) of the row 1437 * @return a fully populated HostAuth or null if the row does not exist 1438 */ 1439 private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) { 1440 Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION, 1441 HostAuth.RECORD_ID + "=?", new String[] {Long.toString(id)}, null, null, null); 1442 try { 1443 if (c.moveToFirst()) { 1444 HostAuth hostAuth = new HostAuth(); 1445 hostAuth.restore(c); 1446 return hostAuth; 1447 } 1448 return null; 1449 } finally { 1450 c.close(); 1451 } 1452 } 1453 1454 /** 1455 * Copy the Account and HostAuth tables from one database to another 1456 * @param fromDatabase the source database 1457 * @param toDatabase the destination database 1458 * @return the number of accounts copied, or -1 if an error occurred 1459 */ 1460 private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) { 1461 if (fromDatabase == null || toDatabase == null) return -1; 1462 1463 // Lock both databases; for the "from" database, we don't want anyone changing it from 1464 // under us; for the "to" database, we want to make the operation atomic 1465 int copyCount = 0; 1466 fromDatabase.beginTransaction(); 1467 try { 1468 toDatabase.beginTransaction(); 1469 try { 1470 // Delete anything hanging around here 1471 toDatabase.delete(Account.TABLE_NAME, null, null); 1472 toDatabase.delete(HostAuth.TABLE_NAME, null, null); 1473 1474 // Get our account cursor 1475 Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION, 1476 null, null, null, null, null); 1477 if (c == null) return 0; 1478 Log.d(TAG, "fromDatabase accounts: " + c.getCount()); 1479 try { 1480 // Loop through accounts, copying them and associated host auth's 1481 while (c.moveToNext()) { 1482 Account account = new Account(); 1483 account.restore(c); 1484 1485 // Clear security sync key and sync key, as these were specific to the 1486 // state of the account, and we've reset that... 1487 // Clear policy key so that we can re-establish policies from the server 1488 // TODO This is pretty EAS specific, but there's a lot of that around 1489 account.mSecuritySyncKey = null; 1490 account.mSyncKey = null; 1491 account.mPolicyKey = 0; 1492 1493 // Copy host auth's and update foreign keys 1494 HostAuth hostAuth = restoreHostAuth(fromDatabase, 1495 account.mHostAuthKeyRecv); 1496 1497 // The account might have gone away, though very unlikely 1498 if (hostAuth == null) continue; 1499 account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null, 1500 hostAuth.toContentValues()); 1501 1502 // EAS accounts have no send HostAuth 1503 if (account.mHostAuthKeySend > 0) { 1504 hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend); 1505 // Belt and suspenders; I can't imagine that this is possible, 1506 // since we checked the validity of the account above, and the 1507 // database is now locked 1508 if (hostAuth == null) continue; 1509 account.mHostAuthKeySend = toDatabase.insert( 1510 HostAuth.TABLE_NAME, null, hostAuth.toContentValues()); 1511 } 1512 1513 // Now, create the account in the "to" database 1514 toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues()); 1515 copyCount++; 1516 } 1517 } finally { 1518 c.close(); 1519 } 1520 1521 // Say it's ok to commit 1522 toDatabase.setTransactionSuccessful(); 1523 } finally { 1524 // STOPSHIP: Remove logging here and in at endTransaction() below 1525 Log.d(TAG, "ending toDatabase transaction; copyCount = " + copyCount); 1526 toDatabase.endTransaction(); 1527 } 1528 } catch (SQLiteException ex) { 1529 Log.w(TAG, "Exception while copying account tables", ex); 1530 copyCount = -1; 1531 } finally { 1532 Log.d(TAG, "ending fromDatabase transaction; copyCount = " + copyCount); 1533 fromDatabase.endTransaction(); 1534 } 1535 return copyCount; 1536 } 1537 1538 private static SQLiteDatabase getBackupDatabase(Context context) { 1539 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, BACKUP_DATABASE_NAME); 1540 return helper.getWritableDatabase(); 1541 } 1542 1543 /** 1544 * Backup account data, returning the number of accounts backed up 1545 */ 1546 private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) { 1547 if (MailActivityEmail.DEBUG) { 1548 Log.d(TAG, "backupAccounts..."); 1549 } 1550 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1551 try { 1552 int numBackedUp = copyAccountTables(mainDatabase, backupDatabase); 1553 if (numBackedUp < 0) { 1554 Log.e(TAG, "Account backup failed!"); 1555 } else if (MailActivityEmail.DEBUG) { 1556 Log.d(TAG, "Backed up " + numBackedUp + " accounts..."); 1557 } 1558 return numBackedUp; 1559 } finally { 1560 if (backupDatabase != null) { 1561 backupDatabase.close(); 1562 } 1563 } 1564 } 1565 1566 /** 1567 * Restore account data, returning the number of accounts restored 1568 */ 1569 private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) { 1570 if (MailActivityEmail.DEBUG) { 1571 Log.d(TAG, "restoreAccounts..."); 1572 } 1573 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1574 try { 1575 int numRecovered = copyAccountTables(backupDatabase, mainDatabase); 1576 if (numRecovered > 0) { 1577 Log.e(TAG, "Recovered " + numRecovered + " accounts!"); 1578 } else if (numRecovered < 0) { 1579 Log.e(TAG, "Account recovery failed?"); 1580 } else if (MailActivityEmail.DEBUG) { 1581 Log.d(TAG, "No accounts to restore..."); 1582 } 1583 return numRecovered; 1584 } finally { 1585 if (backupDatabase != null) { 1586 backupDatabase.close(); 1587 } 1588 } 1589 } 1590 1591 // select count(*) from (select count(*) as dupes from Mailbox where accountKey=? 1592 // group by serverId) where dupes > 1; 1593 private static final String ACCOUNT_INTEGRITY_SQL = 1594 "select count(*) from (select count(*) as dupes from " + Mailbox.TABLE_NAME + 1595 " where accountKey=? group by " + MailboxColumns.SERVER_ID + ") where dupes > 1"; 1596 1597 @Override 1598 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1599 // Handle this special case the fastest possible way 1600 if (uri == INTEGRITY_CHECK_URI) { 1601 checkDatabases(); 1602 return 0; 1603 } else if (uri == ACCOUNT_BACKUP_URI) { 1604 return backupAccounts(getContext(), getDatabase(getContext())); 1605 } 1606 1607 // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID) 1608 Uri notificationUri = EmailContent.CONTENT_URI; 1609 1610 int match = findMatch(uri, "update"); 1611 Context context = getContext(); 1612 ContentResolver resolver = context.getContentResolver(); 1613 // See the comment at delete(), above 1614 SQLiteDatabase db = getDatabase(context); 1615 int table = match >> BASE_SHIFT; 1616 int result; 1617 1618 // We do NOT allow setting of unreadCount/messageCount via the provider 1619 // These columns are maintained via triggers 1620 if (match == MAILBOX_ID || match == MAILBOX) { 1621 values.remove(MailboxColumns.UNREAD_COUNT); 1622 values.remove(MailboxColumns.MESSAGE_COUNT); 1623 } 1624 1625 ContentCache cache = mContentCaches[table]; 1626 String tableName = TABLE_NAMES[table]; 1627 String id = "0"; 1628 1629 try { 1630 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 1631 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 1632 notifyUIConversation(uri); 1633 } 1634 } 1635outer: 1636 switch (match) { 1637 case UI_FOLDER: 1638 return uiUpdateFolder(uri, values); 1639 case UI_RECENT_FOLDERS: 1640 return uiUpdateRecentFolders(uri, values); 1641 case UI_DEFAULT_RECENT_FOLDERS: 1642 return uiPopulateRecentFolders(uri); 1643 case UI_ATTACHMENT: 1644 return uiUpdateAttachment(uri, values); 1645 case UI_UPDATEDRAFT: 1646 return uiUpdateDraft(uri, values); 1647 case UI_SENDDRAFT: 1648 return uiSendDraft(uri, values); 1649 case UI_MESSAGE: 1650 return uiUpdateMessage(uri, values); 1651 case ACCOUNT_CHECK: 1652 id = uri.getLastPathSegment(); 1653 // With any error, return 1 (a failure) 1654 int res = 1; 1655 Cursor ic = null; 1656 try { 1657 ic = db.rawQuery(ACCOUNT_INTEGRITY_SQL, new String[] {id}); 1658 if (ic.moveToFirst()) { 1659 res = ic.getInt(0); 1660 } 1661 } finally { 1662 if (ic != null) { 1663 ic.close(); 1664 } 1665 } 1666 // Count of duplicated mailboxes 1667 return res; 1668 case MAILBOX_ID_ADD_TO_FIELD: 1669 case ACCOUNT_ID_ADD_TO_FIELD: 1670 id = uri.getPathSegments().get(1); 1671 String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME); 1672 Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME); 1673 if (field == null || add == null) { 1674 throw new IllegalArgumentException("No field/add specified " + uri); 1675 } 1676 ContentValues actualValues = new ContentValues(); 1677 if (cache != null) { 1678 cache.lock(id); 1679 } 1680 try { 1681 db.beginTransaction(); 1682 try { 1683 Cursor c = db.query(tableName, 1684 new String[] {EmailContent.RECORD_ID, field}, 1685 whereWithId(id, selection), 1686 selectionArgs, null, null, null); 1687 try { 1688 result = 0; 1689 String[] bind = new String[1]; 1690 if (c.moveToNext()) { 1691 bind[0] = c.getString(0); // _id 1692 long value = c.getLong(1) + add; 1693 actualValues.put(field, value); 1694 result = db.update(tableName, actualValues, ID_EQUALS, bind); 1695 } 1696 db.setTransactionSuccessful(); 1697 } finally { 1698 c.close(); 1699 } 1700 } finally { 1701 db.endTransaction(); 1702 } 1703 } finally { 1704 if (cache != null) { 1705 cache.unlock(id, actualValues); 1706 } 1707 } 1708 break; 1709 case SYNCED_MESSAGE_ID: 1710 case UPDATED_MESSAGE_ID: 1711 case MESSAGE_ID: 1712 case BODY_ID: 1713 case ATTACHMENT_ID: 1714 case MAILBOX_ID: 1715 case ACCOUNT_ID: 1716 case HOSTAUTH_ID: 1717 case QUICK_RESPONSE_ID: 1718 case POLICY_ID: 1719 id = uri.getPathSegments().get(1); 1720 if (cache != null) { 1721 cache.lock(id); 1722 } 1723 try { 1724 if (match == SYNCED_MESSAGE_ID) { 1725 // For synced messages, first copy the old message to the updated table 1726 // Note the insert or ignore semantics, guaranteeing that only the first 1727 // update will be reflected in the updated message table; therefore this 1728 // row will always have the "original" data 1729 db.execSQL(UPDATED_MESSAGE_INSERT + id); 1730 } else if (match == MESSAGE_ID) { 1731 db.execSQL(UPDATED_MESSAGE_DELETE + id); 1732 } 1733 result = db.update(tableName, values, whereWithId(id, selection), 1734 selectionArgs); 1735 } catch (SQLiteException e) { 1736 // Null out values (so they aren't cached) and re-throw 1737 values = null; 1738 throw e; 1739 } finally { 1740 if (cache != null) { 1741 cache.unlock(id, values); 1742 } 1743 } 1744 if (match == ATTACHMENT_ID) { 1745 long attId = Integer.parseInt(id); 1746 if (values.containsKey(Attachment.FLAGS)) { 1747 int flags = values.getAsInteger(Attachment.FLAGS); 1748 mAttachmentService.attachmentChanged(context, attId, flags); 1749 } 1750 // Notify UI if necessary; there are only two columns we can change that 1751 // would be worth a notification 1752 if (values.containsKey(AttachmentColumns.UI_STATE) || 1753 values.containsKey(AttachmentColumns.UI_DOWNLOADED_SIZE)) { 1754 // Notify on individual attachment 1755 notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id); 1756 Attachment att = Attachment.restoreAttachmentWithId(context, attId); 1757 if (att != null) { 1758 // And on owning Message 1759 notifyUI(UIPROVIDER_ATTACHMENTS_NOTIFIER, att.mMessageKey); 1760 } 1761 } 1762 } else if (match == MAILBOX_ID && values.containsKey(Mailbox.UI_SYNC_STATUS)) { 1763 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, id); 1764 } else if (match == ACCOUNT_ID) { 1765 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id); 1766 } 1767 break; 1768 case BODY: 1769 case MESSAGE: 1770 case UPDATED_MESSAGE: 1771 case ATTACHMENT: 1772 case MAILBOX: 1773 case ACCOUNT: 1774 case HOSTAUTH: 1775 case POLICY: 1776 switch(match) { 1777 // To avoid invalidating the cache on updates, we execute them one at a 1778 // time using the XXX_ID uri; these are all executed atomically 1779 case ACCOUNT: 1780 case MAILBOX: 1781 case HOSTAUTH: 1782 case POLICY: 1783 Cursor c = db.query(tableName, EmailContent.ID_PROJECTION, 1784 selection, selectionArgs, null, null, null); 1785 db.beginTransaction(); 1786 result = 0; 1787 try { 1788 while (c.moveToNext()) { 1789 update(ContentUris.withAppendedId( 1790 uri, c.getLong(EmailContent.ID_PROJECTION_COLUMN)), 1791 values, null, null); 1792 result++; 1793 } 1794 db.setTransactionSuccessful(); 1795 } finally { 1796 db.endTransaction(); 1797 c.close(); 1798 } 1799 break outer; 1800 // Any cached table other than those above should be invalidated here 1801 case MESSAGE: 1802 // If we're doing some generic update, the whole cache needs to be 1803 // invalidated. This case should be quite rare 1804 cache.invalidate("Update", uri, selection); 1805 //$FALL-THROUGH$ 1806 default: 1807 result = db.update(tableName, values, selection, selectionArgs); 1808 break outer; 1809 } 1810 case ACCOUNT_RESET_NEW_COUNT_ID: 1811 id = uri.getPathSegments().get(1); 1812 if (cache != null) { 1813 cache.lock(id); 1814 } 1815 ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 1816 if (values != null) { 1817 Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME); 1818 if (set != null) { 1819 newMessageCount = new ContentValues(); 1820 newMessageCount.put(Account.NEW_MESSAGE_COUNT, set); 1821 } 1822 } 1823 try { 1824 result = db.update(tableName, newMessageCount, 1825 whereWithId(id, selection), selectionArgs); 1826 } finally { 1827 if (cache != null) { 1828 cache.unlock(id, values); 1829 } 1830 } 1831 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1832 break; 1833 case ACCOUNT_RESET_NEW_COUNT: 1834 result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT, 1835 selection, selectionArgs); 1836 // Affects all accounts. Just invalidate all account cache. 1837 cache.invalidate("Reset all new counts", null, null); 1838 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1839 break; 1840 default: 1841 throw new IllegalArgumentException("Unknown URI " + uri); 1842 } 1843 } catch (SQLiteException e) { 1844 checkDatabases(); 1845 throw e; 1846 } 1847 1848 // Notify all notifier cursors 1849 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id); 1850 1851 resolver.notifyChange(notificationUri, null); 1852 return result; 1853 } 1854 1855 /** 1856 * Returns the base notification URI for the given content type. 1857 * 1858 * @param match The type of content that was modified. 1859 */ 1860 private Uri getBaseNotificationUri(int match) { 1861 Uri baseUri = null; 1862 switch (match) { 1863 case MESSAGE: 1864 case MESSAGE_ID: 1865 case SYNCED_MESSAGE_ID: 1866 baseUri = Message.NOTIFIER_URI; 1867 break; 1868 case ACCOUNT: 1869 case ACCOUNT_ID: 1870 baseUri = Account.NOTIFIER_URI; 1871 break; 1872 } 1873 return baseUri; 1874 } 1875 1876 /** 1877 * Sends a change notification to any cursors observers of the given base URI. The final 1878 * notification URI is dynamically built to contain the specified information. It will be 1879 * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending 1880 * upon the given values. 1881 * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked. 1882 * If this is necessary, it can be added. However, due to the implementation of 1883 * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications. 1884 * 1885 * @param baseUri The base URI to send notifications to. Must be able to take appended IDs. 1886 * @param op Optional operation to be appended to the URI. 1887 * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be 1888 * appended to the base URI. 1889 */ 1890 private void sendNotifierChange(Uri baseUri, String op, String id) { 1891 if (baseUri == null) return; 1892 1893 final ContentResolver resolver = getContext().getContentResolver(); 1894 1895 // Append the operation, if specified 1896 if (op != null) { 1897 baseUri = baseUri.buildUpon().appendEncodedPath(op).build(); 1898 } 1899 1900 long longId = 0L; 1901 try { 1902 longId = Long.valueOf(id); 1903 } catch (NumberFormatException ignore) {} 1904 if (longId > 0) { 1905 resolver.notifyChange(ContentUris.withAppendedId(baseUri, longId), null); 1906 } else { 1907 resolver.notifyChange(baseUri, null); 1908 } 1909 1910 // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI. 1911 if (baseUri.equals(Message.NOTIFIER_URI)) { 1912 sendMessageListDataChangedNotification(); 1913 } 1914 } 1915 1916 private void sendMessageListDataChangedNotification() { 1917 final Context context = getContext(); 1918 final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED); 1919 // Ideally this intent would contain information about which account changed, to limit the 1920 // updates to that particular account. Unfortunately, that information is not available in 1921 // sendNotifierChange(). 1922 context.sendBroadcast(intent); 1923 } 1924 1925 @Override 1926 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1927 throws OperationApplicationException { 1928 Context context = getContext(); 1929 SQLiteDatabase db = getDatabase(context); 1930 db.beginTransaction(); 1931 try { 1932 ContentProviderResult[] results = super.applyBatch(operations); 1933 db.setTransactionSuccessful(); 1934 return results; 1935 } finally { 1936 db.endTransaction(); 1937 } 1938 } 1939 1940 /** 1941 * For testing purposes, check whether a given row is cached 1942 * @param baseUri the base uri of the EmailContent 1943 * @param id the row id of the EmailContent 1944 * @return whether or not the row is currently cached 1945 */ 1946 @VisibleForTesting 1947 protected boolean isCached(Uri baseUri, long id) { 1948 int match = findMatch(baseUri, "isCached"); 1949 int table = match >> BASE_SHIFT; 1950 ContentCache cache = mContentCaches[table]; 1951 if (cache == null) return false; 1952 Cursor cc = cache.get(Long.toString(id)); 1953 return (cc != null); 1954 } 1955 1956 public static interface AttachmentService { 1957 /** 1958 * Notify the service that an attachment has changed. 1959 */ 1960 void attachmentChanged(Context context, long id, int flags); 1961 } 1962 1963 private final AttachmentService DEFAULT_ATTACHMENT_SERVICE = new AttachmentService() { 1964 @Override 1965 public void attachmentChanged(Context context, long id, int flags) { 1966 // The default implementation delegates to the real service. 1967 AttachmentDownloadService.attachmentChanged(context, id, flags); 1968 } 1969 }; 1970 private AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE; 1971 1972 /** 1973 * Injects a custom attachment service handler. If null is specified, will reset to the 1974 * default service. 1975 */ 1976 public void injectAttachmentService(AttachmentService as) { 1977 mAttachmentService = (as == null) ? DEFAULT_ATTACHMENT_SERVICE : as; 1978 } 1979 1980 // SELECT DISTINCT Boxes._id, Boxes.unreadCount count(Message._id) from Message, 1981 // (SELECT _id, unreadCount, messageCount, lastNotifiedMessageCount, lastNotifiedMessageKey 1982 // FROM Mailbox WHERE accountKey=6 AND ((type = 0) OR (syncInterval!=0 AND syncInterval!=-1))) 1983 // AS Boxes 1984 // WHERE Boxes.messageCount!=Boxes.lastNotifiedMessageCount 1985 // OR (Boxes._id=Message.mailboxKey AND Message._id>Boxes.lastNotifiedMessageKey) 1986 // TODO: This query can be simplified a bit 1987 private static final String NOTIFICATION_QUERY = 1988 "SELECT DISTINCT Boxes." + MailboxColumns.ID + ", Boxes." + MailboxColumns.UNREAD_COUNT + 1989 ", count(" + Message.TABLE_NAME + "." + MessageColumns.ID + ")" + 1990 " FROM " + 1991 Message.TABLE_NAME + "," + 1992 "(SELECT " + MailboxColumns.ID + "," + MailboxColumns.UNREAD_COUNT + "," + 1993 MailboxColumns.MESSAGE_COUNT + "," + MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT + 1994 "," + MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY + " FROM " + Mailbox.TABLE_NAME + 1995 " WHERE " + MailboxColumns.ACCOUNT_KEY + "=?" + 1996 " AND (" + MailboxColumns.TYPE + "=" + Mailbox.TYPE_INBOX + " OR (" 1997 + MailboxColumns.SYNC_INTERVAL + "!=0 AND " + 1998 MailboxColumns.SYNC_INTERVAL + "!=-1))) AS Boxes " + 1999 "WHERE Boxes." + MailboxColumns.ID + '=' + Message.TABLE_NAME + "." + 2000 MessageColumns.MAILBOX_KEY + " AND " + Message.TABLE_NAME + "." + 2001 MessageColumns.ID + ">Boxes." + MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY + 2002 " AND " + MessageColumns.FLAG_READ + "=0 AND " + MessageColumns.TIMESTAMP + "!=0"; 2003 2004 public Cursor notificationQuery(Uri uri) { 2005 SQLiteDatabase db = getDatabase(getContext()); 2006 String accountId = uri.getLastPathSegment(); 2007 return db.rawQuery(NOTIFICATION_QUERY, new String[] {accountId}); 2008 } 2009 2010 public Cursor mostRecentMessageQuery(Uri uri) { 2011 SQLiteDatabase db = getDatabase(getContext()); 2012 String mailboxId = uri.getLastPathSegment(); 2013 return db.rawQuery("select max(_id) from Message where mailboxKey=?", 2014 new String[] {mailboxId}); 2015 } 2016 2017 /** 2018 * Support for UnifiedEmail below 2019 */ 2020 2021 private static final String NOT_A_DRAFT_STRING = 2022 Integer.toString(UIProvider.DraftType.NOT_A_DRAFT); 2023 2024 private static final String CONVERSATION_FLAGS = 2025 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE + 2026 ") !=0 THEN " + UIProvider.ConversationFlags.CALENDAR_INVITE + 2027 " ELSE 0 END + " + 2028 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_FORWARDED + 2029 ") !=0 THEN " + UIProvider.ConversationFlags.FORWARDED + 2030 " ELSE 0 END + " + 2031 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_REPLIED_TO + 2032 ") !=0 THEN " + UIProvider.ConversationFlags.REPLIED + 2033 " ELSE 0 END"; 2034 2035 /** 2036 * Array of pre-defined account colors (legacy colors from old email app) 2037 */ 2038 private static final int[] ACCOUNT_COLORS = new int[] { 2039 0xff71aea7, 0xff621919, 0xff18462f, 0xffbf8e52, 0xff001f79, 2040 0xffa8afc2, 0xff6b64c4, 0xff738359, 0xff9d50a4 2041 }; 2042 2043 private static final String CONVERSATION_COLOR = 2044 "@CASE (" + MessageColumns.ACCOUNT_KEY + " - 1) % " + ACCOUNT_COLORS.length + 2045 " WHEN 0 THEN " + ACCOUNT_COLORS[0] + 2046 " WHEN 1 THEN " + ACCOUNT_COLORS[1] + 2047 " WHEN 2 THEN " + ACCOUNT_COLORS[2] + 2048 " WHEN 3 THEN " + ACCOUNT_COLORS[3] + 2049 " WHEN 4 THEN " + ACCOUNT_COLORS[4] + 2050 " WHEN 5 THEN " + ACCOUNT_COLORS[5] + 2051 " WHEN 6 THEN " + ACCOUNT_COLORS[6] + 2052 " WHEN 7 THEN " + ACCOUNT_COLORS[7] + 2053 " WHEN 8 THEN " + ACCOUNT_COLORS[8] + 2054 " END"; 2055 2056 private static final String ACCOUNT_COLOR = 2057 "@CASE (" + AccountColumns.ID + " - 1) % " + ACCOUNT_COLORS.length + 2058 " WHEN 0 THEN " + ACCOUNT_COLORS[0] + 2059 " WHEN 1 THEN " + ACCOUNT_COLORS[1] + 2060 " WHEN 2 THEN " + ACCOUNT_COLORS[2] + 2061 " WHEN 3 THEN " + ACCOUNT_COLORS[3] + 2062 " WHEN 4 THEN " + ACCOUNT_COLORS[4] + 2063 " WHEN 5 THEN " + ACCOUNT_COLORS[5] + 2064 " WHEN 6 THEN " + ACCOUNT_COLORS[6] + 2065 " WHEN 7 THEN " + ACCOUNT_COLORS[7] + 2066 " WHEN 8 THEN " + ACCOUNT_COLORS[8] + 2067 " END"; 2068 /** 2069 * Mapping of UIProvider columns to EmailProvider columns for the message list (called the 2070 * conversation list in UnifiedEmail) 2071 */ 2072 private static final ProjectionMap sMessageListMap = ProjectionMap.builder() 2073 .add(BaseColumns._ID, MessageColumns.ID) 2074 .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage")) 2075 .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage")) 2076 .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT) 2077 .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET) 2078 .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST) 2079 .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) 2080 .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) 2081 .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1") 2082 .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0") 2083 .add(UIProvider.ConversationColumns.SENDING_STATE, 2084 Integer.toString(ConversationSendingState.OTHER)) 2085 .add(UIProvider.ConversationColumns.PRIORITY, Integer.toString(ConversationPriority.LOW)) 2086 .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ) 2087 .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE) 2088 .add(UIProvider.ConversationColumns.FOLDER_LIST, 2089 "'content://" + EmailContent.AUTHORITY + "/uifolder/' || " 2090 + MessageColumns.MAILBOX_KEY) 2091 .add(UIProvider.ConversationColumns.FLAGS, CONVERSATION_FLAGS) 2092 .add(UIProvider.ConversationColumns.ACCOUNT_URI, 2093 "'content://" + EmailContent.AUTHORITY + "/uiaccount/' || " 2094 + MessageColumns.ACCOUNT_KEY) 2095 .build(); 2096 2097 /** 2098 * Generate UIProvider draft type; note the test for "reply all" must come before "reply" 2099 */ 2100 private static final String MESSAGE_DRAFT_TYPE = 2101 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_ORIGINAL + 2102 ") !=0 THEN " + UIProvider.DraftType.COMPOSE + 2103 " WHEN (" + MessageColumns.FLAGS + "&" + (1<<20) + 2104 ") !=0 THEN " + UIProvider.DraftType.REPLY_ALL + 2105 " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY + 2106 ") !=0 THEN " + UIProvider.DraftType.REPLY + 2107 " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_FORWARD + 2108 ") !=0 THEN " + UIProvider.DraftType.FORWARD + 2109 " ELSE " + UIProvider.DraftType.NOT_A_DRAFT + " END"; 2110 2111 private static final String MESSAGE_FLAGS = 2112 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE + 2113 ") !=0 THEN " + UIProvider.MessageFlags.CALENDAR_INVITE + 2114 " ELSE 0 END"; 2115 2116 /** 2117 * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in 2118 * UnifiedEmail 2119 */ 2120 private static final ProjectionMap sMessageViewMap = ProjectionMap.builder() 2121 .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID) 2122 .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID) 2123 .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME)) 2124 .add(UIProvider.MessageColumns.CONVERSATION_ID, 2125 uriWithFQId("uimessage", Message.TABLE_NAME)) 2126 .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT) 2127 .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET) 2128 .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST) 2129 .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST) 2130 .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST) 2131 .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST) 2132 .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST) 2133 .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, EmailContent.MessageColumns.TIMESTAMP) 2134 .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT) 2135 .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT) 2136 .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0") 2137 .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING) 2138 .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0") 2139 .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, EmailContent.MessageColumns.FLAG_ATTACHMENT) 2140 .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, 2141 uriWithFQId("uiattachments", Message.TABLE_NAME)) 2142 .add(UIProvider.MessageColumns.MESSAGE_FLAGS, MESSAGE_FLAGS) 2143 .add(UIProvider.MessageColumns.SAVE_MESSAGE_URI, 2144 uriWithFQId("uiupdatedraft", Message.TABLE_NAME)) 2145 .add(UIProvider.MessageColumns.SEND_MESSAGE_URI, 2146 uriWithFQId("uisenddraft", Message.TABLE_NAME)) 2147 .add(UIProvider.MessageColumns.DRAFT_TYPE, MESSAGE_DRAFT_TYPE) 2148 .add(UIProvider.MessageColumns.MESSAGE_ACCOUNT_URI, 2149 uriWithColumn("account", MessageColumns.ACCOUNT_KEY)) 2150 .add(UIProvider.MessageColumns.STARRED, EmailContent.MessageColumns.FLAG_FAVORITE) 2151 .add(UIProvider.MessageColumns.READ, EmailContent.MessageColumns.FLAG_READ) 2152 .build(); 2153 2154 /** 2155 * Generate UIProvider folder capabilities from mailbox flags 2156 */ 2157 private static final String FOLDER_CAPABILITIES = 2158 "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL + 2159 ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES + 2160 " ELSE 0 END"; 2161 2162 /** 2163 * Convert EmailProvider type to UIProvider type 2164 */ 2165 private static final String FOLDER_TYPE = "CASE " + MailboxColumns.TYPE 2166 + " WHEN " + Mailbox.TYPE_INBOX + " THEN " + UIProvider.FolderType.INBOX 2167 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN " + UIProvider.FolderType.DRAFT 2168 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN " + UIProvider.FolderType.OUTBOX 2169 + " WHEN " + Mailbox.TYPE_SENT + " THEN " + UIProvider.FolderType.SENT 2170 + " WHEN " + Mailbox.TYPE_TRASH + " THEN " + UIProvider.FolderType.TRASH 2171 + " WHEN " + Mailbox.TYPE_JUNK + " THEN " + UIProvider.FolderType.SPAM 2172 + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + UIProvider.FolderType.STARRED 2173 + " ELSE " + UIProvider.FolderType.DEFAULT + " END"; 2174 2175 private static final String FOLDER_ICON = "CASE " + MailboxColumns.TYPE 2176 + " WHEN " + Mailbox.TYPE_INBOX + " THEN " + R.drawable.ic_folder_inbox_holo_light 2177 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN " + R.drawable.ic_folder_drafts_holo_light 2178 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN " + R.drawable.ic_folder_outbox_holo_light 2179 + " WHEN " + Mailbox.TYPE_SENT + " THEN " + R.drawable.ic_folder_sent_holo_light 2180 + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + R.drawable.ic_menu_star_holo_light 2181 + " ELSE -1 END"; 2182 2183 private static final ProjectionMap sFolderListMap = ProjectionMap.builder() 2184 .add(BaseColumns._ID, MailboxColumns.ID) 2185 .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) 2186 .add(UIProvider.FolderColumns.NAME, "displayName") 2187 .add(UIProvider.FolderColumns.HAS_CHILDREN, 2188 MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN) 2189 .add(UIProvider.FolderColumns.CAPABILITIES, FOLDER_CAPABILITIES) 2190 .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") 2191 .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) 2192 .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders")) 2193 .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT) 2194 .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.MESSAGE_COUNT) 2195 .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId("uirefresh")) 2196 .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS) 2197 .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT) 2198 .add(UIProvider.FolderColumns.TYPE, FOLDER_TYPE) 2199 .add(UIProvider.FolderColumns.ICON_RES_ID, FOLDER_ICON) 2200 .add(UIProvider.FolderColumns.HIERARCHICAL_DESC, MailboxColumns.HIERARCHICAL_NAME) 2201 .build(); 2202 2203 private static final ProjectionMap sAccountListMap = ProjectionMap.builder() 2204 .add(BaseColumns._ID, AccountColumns.ID) 2205 .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders")) 2206 .add(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, uriWithId("uiallfolders")) 2207 .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME) 2208 .add(UIProvider.AccountColumns.SAVE_DRAFT_URI, uriWithId("uisavedraft")) 2209 .add(UIProvider.AccountColumns.SEND_MAIL_URI, uriWithId("uisendmail")) 2210 .add(UIProvider.AccountColumns.UNDO_URI, 2211 ("'content://" + UIProvider.AUTHORITY + "/uiundo'")) 2212 .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount")) 2213 .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch")) 2214 // TODO: Is provider version used? 2215 .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1") 2216 .add(UIProvider.AccountColumns.SYNC_STATUS, "0") 2217 .add(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI, uriWithId("uirecentfolders")) 2218 .add(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI, 2219 uriWithId("uidefaultrecentfolders")) 2220 .add(UIProvider.AccountColumns.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE) 2221 .add(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS, 2222 Integer.toString(UIProvider.SnapHeaderValue.ALWAYS)) 2223 .add(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR, 2224 Integer.toString(UIProvider.DefaultReplyBehavior.REPLY)) 2225 .add(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, "0") 2226 .build(); 2227 2228 /** 2229 * The "ORDER BY" clause for top level folders 2230 */ 2231 private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE 2232 + " WHEN " + Mailbox.TYPE_INBOX + " THEN 0" 2233 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN 1" 2234 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN 2" 2235 + " WHEN " + Mailbox.TYPE_SENT + " THEN 3" 2236 + " WHEN " + Mailbox.TYPE_TRASH + " THEN 4" 2237 + " WHEN " + Mailbox.TYPE_JUNK + " THEN 5" 2238 // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order. 2239 + " ELSE 10 END" 2240 + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 2241 2242 /** 2243 * Mapping of UIProvider columns to EmailProvider columns for a message's attachments 2244 */ 2245 private static final ProjectionMap sAttachmentMap = ProjectionMap.builder() 2246 .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME) 2247 .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE) 2248 .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment")) 2249 .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE) 2250 .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE) 2251 .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION) 2252 .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE, AttachmentColumns.UI_DOWNLOADED_SIZE) 2253 .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI) 2254 .build(); 2255 2256 /** 2257 * Generate the SELECT clause using a specified mapping and the original UI projection 2258 * @param map the ProjectionMap to use for this projection 2259 * @param projection the projection as sent by UnifiedEmail 2260 * @param values ContentValues to be used if the ProjectionMap entry is null 2261 * @return a StringBuilder containing the SELECT expression for a SQLite query 2262 */ 2263 private StringBuilder genSelect(ProjectionMap map, String[] projection) { 2264 return genSelect(map, projection, EMPTY_CONTENT_VALUES); 2265 } 2266 2267 private StringBuilder genSelect(ProjectionMap map, String[] projection, ContentValues values) { 2268 StringBuilder sb = new StringBuilder("SELECT "); 2269 boolean first = true; 2270 for (String column: projection) { 2271 if (first) { 2272 first = false; 2273 } else { 2274 sb.append(','); 2275 } 2276 String val = null; 2277 // First look at values; this is an override of default behavior 2278 if (values.containsKey(column)) { 2279 String value = values.getAsString(column); 2280 if (value.startsWith("@")) { 2281 val = value.substring(1) + " AS " + column; 2282 } else { 2283 val = "'" + values.getAsString(column) + "' AS " + column; 2284 } 2285 } else { 2286 // Now, get the standard value for the column from our projection map 2287 val = map.get(column); 2288 // If we don't have the column, return "NULL AS <column>", and warn 2289 if (val == null) { 2290 val = "NULL AS " + column; 2291 } 2292 } 2293 sb.append(val); 2294 } 2295 return sb; 2296 } 2297 2298 /** 2299 * Convenience method to create a Uri string given the "type" of query; we append the type 2300 * of the query and the id column name (_id) 2301 * 2302 * @param type the "type" of the query, as defined by our UriMatcher definitions 2303 * @return a Uri string 2304 */ 2305 private static String uriWithId(String type) { 2306 return uriWithColumn(type, EmailContent.RECORD_ID); 2307 } 2308 2309 /** 2310 * Convenience method to create a Uri string given the "type" of query; we append the type 2311 * of the query and the passed in column name 2312 * 2313 * @param type the "type" of the query, as defined by our UriMatcher definitions 2314 * @param columnName the column in the table being queried 2315 * @return a Uri string 2316 */ 2317 private static String uriWithColumn(String type, String columnName) { 2318 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + columnName; 2319 } 2320 2321 /** 2322 * Convenience method to create a Uri string given the "type" of query and the table name to 2323 * which it applies; we append the type of the query and the fully qualified (FQ) id column 2324 * (i.e. including the table name); we need this for join queries where _id would otherwise 2325 * be ambiguous 2326 * 2327 * @param type the "type" of the query, as defined by our UriMatcher definitions 2328 * @param tableName the name of the table whose _id is referred to 2329 * @return a Uri string 2330 */ 2331 private static String uriWithFQId(String type, String tableName) { 2332 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id"; 2333 } 2334 2335 // Regex that matches start of img tag. '<(?i)img\s+'. 2336 private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+"); 2337 2338 /** 2339 * Class that holds the sqlite query and the attachment (JSON) value (which might be null)g 2340 */ 2341 private static class MessageQuery { 2342 final String query; 2343 final String attachmentJson; 2344 2345 MessageQuery(String _query, String _attachmentJson) { 2346 query = _query; 2347 attachmentJson = _attachmentJson; 2348 } 2349 } 2350 2351 /** 2352 * Generate the "view message" SQLite query, given a projection from UnifiedEmail 2353 * 2354 * @param uiProjection as passed from UnifiedEmail 2355 * @return the SQLite query to be executed on the EmailProvider database 2356 */ 2357 private MessageQuery genQueryViewMessage(String[] uiProjection, String id) { 2358 Context context = getContext(); 2359 long messageId = Long.parseLong(id); 2360 Message msg = Message.restoreMessageWithId(context, messageId); 2361 ContentValues values = new ContentValues(); 2362 String attachmentJson = null; 2363 if (msg != null) { 2364 if (msg.mFlagLoaded == Message.FLAG_LOADED_PARTIAL) { 2365 EmailServiceProxy service = 2366 EmailServiceUtils.getServiceForAccount(context, null, msg.mAccountKey); 2367 try { 2368 service.loadMore(messageId); 2369 } catch (RemoteException e) { 2370 // Nothing to do 2371 } 2372 } 2373 Body body = Body.restoreBodyWithMessageId(context, messageId); 2374 if (body != null) { 2375 if (body.mHtmlContent != null) { 2376 if (IMG_TAG_START_REGEX.matcher(body.mHtmlContent).find()) { 2377 values.put(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, 1); 2378 } 2379 } 2380 } 2381 Address[] fromList = Address.unpack(msg.mFrom); 2382 int autoShowImages = 0; 2383 Preferences prefs = Preferences.getPreferences(context); 2384 for (Address sender : fromList) { 2385 String email = sender.getAddress(); 2386 if (prefs.shouldShowImagesFor(email)) { 2387 autoShowImages = 1; 2388 break; 2389 } 2390 } 2391 values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, autoShowImages); 2392 // Add attachments... 2393 Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId); 2394 if (atts.length > 0) { 2395 ArrayList<com.android.mail.providers.Attachment> uiAtts = 2396 new ArrayList<com.android.mail.providers.Attachment>(); 2397 for (Attachment att : atts) { 2398 if (att.mContentId != null && att.mContentUri != null) { 2399 continue; 2400 } 2401 com.android.mail.providers.Attachment uiAtt = 2402 new com.android.mail.providers.Attachment(); 2403 uiAtt.name = att.mFileName; 2404 uiAtt.contentType = att.mMimeType; 2405 uiAtt.size = (int) att.mSize; 2406 uiAtt.uri = uiUri("uiattachment", att.mId); 2407 uiAtts.add(uiAtt); 2408 } 2409 values.put(UIProvider.MessageColumns.ATTACHMENTS, "@?"); // @ for literal 2410 attachmentJson = com.android.mail.providers.Attachment.toJSONArray(uiAtts); 2411 } 2412 if (msg.mDraftInfo != 0) { 2413 values.put(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, 2414 (msg.mDraftInfo & Message.DRAFT_INFO_APPEND_REF_MESSAGE) != 0 ? 1 : 0); 2415 values.put(UIProvider.MessageColumns.QUOTE_START_POS, 2416 msg.mDraftInfo & Message.DRAFT_INFO_QUOTE_POS_MASK); 2417 } 2418 } 2419 if ((msg.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) { 2420 values.put(UIProvider.MessageColumns.EVENT_INTENT_URI, 2421 "content://ui.email2.android.com/event/" + msg.mId); 2422 } 2423 StringBuilder sb = genSelect(sMessageViewMap, uiProjection, values); 2424 sb.append(" FROM " + Message.TABLE_NAME + "," + Body.TABLE_NAME + " WHERE " + 2425 Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " AND " + 2426 Message.TABLE_NAME + "." + Message.RECORD_ID + "=?"); 2427 String sql = sb.toString(); 2428 return new MessageQuery(sql, attachmentJson); 2429 } 2430 2431 /** 2432 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 2433 * 2434 * @param uiProjection as passed from UnifiedEmail 2435 * @return the SQLite query to be executed on the EmailProvider database 2436 */ 2437 private String genQueryMailboxMessages(String[] uiProjection) { 2438 StringBuilder sb = genSelect(sMessageListMap, uiProjection); 2439 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.MAILBOX_KEY + "=? ORDER BY " + 2440 MessageColumns.TIMESTAMP + " DESC"); 2441 return sb.toString(); 2442 } 2443 2444 /** 2445 * Generate various virtual mailbox SQLite queries, given a projection from UnifiedEmail 2446 * 2447 * @param uiProjection as passed from UnifiedEmail 2448 * @param id the id of the virtual mailbox 2449 * @return the SQLite query to be executed on the EmailProvider database 2450 */ 2451 private Cursor getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection, 2452 long mailboxId) { 2453 ContentValues values = new ContentValues(); 2454 values.put(UIProvider.ConversationColumns.COLOR, CONVERSATION_COLOR); 2455 StringBuilder sb = genSelect(sMessageListMap, uiProjection, values); 2456 if (isCombinedMailbox(mailboxId)) { 2457 switch (getVirtualMailboxType(mailboxId)) { 2458 case Mailbox.TYPE_INBOX: 2459 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2460 MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID + 2461 " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + 2462 "=" + Mailbox.TYPE_INBOX + ") ORDER BY " + MessageColumns.TIMESTAMP + 2463 " DESC"); 2464 break; 2465 case Mailbox.TYPE_STARRED: 2466 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2467 MessageColumns.FLAG_FAVORITE + "=1 ORDER BY " + 2468 MessageColumns.TIMESTAMP + " DESC"); 2469 break; 2470 default: 2471 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); 2472 } 2473 return db.rawQuery(sb.toString(), null); 2474 } else { 2475 switch (getVirtualMailboxType(mailboxId)) { 2476 case Mailbox.TYPE_STARRED: 2477 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2478 MessageColumns.ACCOUNT_KEY + "=? AND " + 2479 MessageColumns.FLAG_FAVORITE + "=1 ORDER BY " + 2480 MessageColumns.TIMESTAMP + " DESC"); 2481 break; 2482 default: 2483 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); 2484 } 2485 return db.rawQuery(sb.toString(), 2486 new String[] {getVirtualMailboxAccountIdString(mailboxId)}); 2487 } 2488 } 2489 2490 /** 2491 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 2492 * 2493 * @param uiProjection as passed from UnifiedEmail 2494 * @return the SQLite query to be executed on the EmailProvider database 2495 */ 2496 private String genQueryConversation(String[] uiProjection) { 2497 StringBuilder sb = genSelect(sMessageListMap, uiProjection); 2498 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.RECORD_ID + "=?"); 2499 return sb.toString(); 2500 } 2501 2502 /** 2503 * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail 2504 * 2505 * @param uiProjection as passed from UnifiedEmail 2506 * @return the SQLite query to be executed on the EmailProvider database 2507 */ 2508 private String genQueryAccountMailboxes(String[] uiProjection) { 2509 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 2510 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2511 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2512 " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY "); 2513 sb.append(MAILBOX_ORDER_BY); 2514 return sb.toString(); 2515 } 2516 2517 /** 2518 * Generate the "all folders" SQLite query, given a projection from UnifiedEmail. The list is 2519 * sorted by the name as it appears in a hierarchical listing 2520 * 2521 * @param uiProjection as passed from UnifiedEmail 2522 * @return the SQLite query to be executed on the EmailProvider database 2523 */ 2524 private String genQueryAccountAllMailboxes(String[] uiProjection) { 2525 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 2526 // Use a derived column to choose either hierarchicalName or displayName 2527 sb.append(", case when " + MailboxColumns.HIERARCHICAL_NAME + " is null then " + 2528 MailboxColumns.DISPLAY_NAME + " else " + MailboxColumns.HIERARCHICAL_NAME + 2529 " end as h_name"); 2530 // Order by the derived column 2531 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2532 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2533 " ORDER BY h_name"); 2534 return sb.toString(); 2535 } 2536 2537 /** 2538 * Generate the "recent folder list" SQLite query, given a projection from UnifiedEmail 2539 * 2540 * @param uiProjection as passed from UnifiedEmail 2541 * @return the SQLite query to be executed on the EmailProvider database 2542 */ 2543 private String genQueryRecentMailboxes(String[] uiProjection) { 2544 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 2545 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2546 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2547 " AND " + MailboxColumns.PARENT_KEY + " < 0 AND " + 2548 MailboxColumns.LAST_TOUCHED_TIME + " > 0 ORDER BY " + 2549 MailboxColumns.LAST_TOUCHED_TIME + " DESC"); 2550 return sb.toString(); 2551 } 2552 2553 /** 2554 * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail 2555 * 2556 * @param uiProjection as passed from UnifiedEmail 2557 * @return the SQLite query to be executed on the EmailProvider database 2558 */ 2559 private String genQueryMailbox(String[] uiProjection, String id) { 2560 long mailboxId = Long.parseLong(id); 2561 ContentValues values = new ContentValues(); 2562 if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) { 2563 // This is the current search mailbox; use the total count 2564 values = new ContentValues(); 2565 values.put(UIProvider.FolderColumns.TOTAL_COUNT, mSearchParams.mTotalCount); 2566 // "load more" is valid for search results 2567 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 2568 uiUriString("uiloadmore", mailboxId)); 2569 } else { 2570 Context context = getContext(); 2571 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 2572 // Make sure we can't get NPE if mailbox has disappeared (the result will end up moot) 2573 if (mailbox != null) { 2574 String protocol = Account.getProtocol(context, mailbox.mAccountKey); 2575 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 2576 // "load more" is valid for protocols not supporting "lookback" 2577 if (info != null && !info.offerLookback) { 2578 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 2579 uiUriString("uiloadmore", mailboxId)); 2580 } else { 2581 int caps = UIProvider.FolderCapabilities.SUPPORTS_SETTINGS; 2582 if ((mailbox.mFlags & Mailbox.FLAG_ACCEPTS_MOVED_MAIL) != 0) { 2583 caps |= UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES; 2584 } 2585 values.put(UIProvider.FolderColumns.CAPABILITIES, caps); 2586 } 2587 // For trash, we don't allow undo 2588 if (mailbox.mType == Mailbox.TYPE_TRASH) { 2589 values.put(UIProvider.FolderColumns.CAPABILITIES, 2590 UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES | 2591 UIProvider.FolderCapabilities.CAN_HOLD_MAIL | 2592 UIProvider.FolderCapabilities.DELETE_ACTION_FINAL); 2593 } 2594 if (isVirtualMailbox(mailboxId)) { 2595 int capa = values.getAsInteger(UIProvider.FolderColumns.CAPABILITIES); 2596 values.put(UIProvider.FolderColumns.CAPABILITIES, 2597 capa | UIProvider.FolderCapabilities.IS_VIRTUAL); 2598 } 2599 } 2600 } 2601 StringBuilder sb = genSelect(sFolderListMap, uiProjection, values); 2602 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ID + "=?"); 2603 return sb.toString(); 2604 } 2605 2606 private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://ui.email.android.com"); 2607 2608 private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com"); 2609 2610 private static String getExternalUriString(String segment, String account) { 2611 return BASE_EXTERNAL_URI.buildUpon().appendPath(segment) 2612 .appendQueryParameter("account", account).build().toString(); 2613 } 2614 2615 private static String getExternalUriStringEmail2(String segment, String account) { 2616 return BASE_EXTERAL_URI2.buildUpon().appendPath(segment) 2617 .appendQueryParameter("account", account).build().toString(); 2618 } 2619 2620 /** 2621 * Generate a "single account" SQLite query, given a projection from UnifiedEmail 2622 * 2623 * @param uiProjection as passed from UnifiedEmail 2624 * @return the SQLite query to be executed on the EmailProvider database 2625 */ 2626 private String genQueryAccount(String[] uiProjection, String id) { 2627 ContentValues values = new ContentValues(); 2628 long accountId = Long.parseLong(id); 2629 2630 // Get account capabilities from the service 2631 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(getContext(), 2632 mServiceCallback, accountId); 2633 int capabilities = 0; 2634 try { 2635 capabilities = service.getCapabilities(accountId); 2636 } catch (RemoteException e) { 2637 } 2638 values.put(UIProvider.AccountColumns.CAPABILITIES, capabilities); 2639 2640 values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI, 2641 getExternalUriString("settings", id)); 2642 values.put(UIProvider.AccountColumns.COMPOSE_URI, 2643 getExternalUriStringEmail2("compose", id)); 2644 values.put(UIProvider.AccountColumns.MIME_TYPE, EMAIL_APP_MIME_TYPE); 2645 values.put(UIProvider.AccountColumns.COLOR, ACCOUNT_COLOR); 2646 2647 Preferences prefs = Preferences.getPreferences(getContext()); 2648 values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE, 2649 prefs.getConfirmDelete() ? "1" : "0"); 2650 values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND, 2651 prefs.getConfirmSend() ? "1" : "0"); 2652 values.put(UIProvider.AccountColumns.SettingsColumns.HIDE_CHECKBOXES, 2653 prefs.getHideCheckboxes() ? "1" : "0"); 2654 int autoAdvance = prefs.getAutoAdvanceDirection(); 2655 values.put(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE, 2656 autoAdvanceToUiValue(autoAdvance)); 2657 int textZoom = prefs.getTextZoom(); 2658 values.put(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE, 2659 textZoomToUiValue(textZoom)); 2660 // Set default inbox, if we've got an inbox; otherwise, say initial sync needed 2661 long mailboxId = Mailbox.findMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX); 2662 if (mailboxId != Mailbox.NO_MAILBOX) { 2663 values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX, 2664 uiUriString("uifolder", mailboxId)); 2665 values.put(UIProvider.AccountColumns.SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC); 2666 } else { 2667 values.put(UIProvider.AccountColumns.SYNC_STATUS, 2668 UIProvider.SyncStatus.INITIAL_SYNC_NEEDED); 2669 } 2670 2671 StringBuilder sb = genSelect(sAccountListMap, uiProjection, values); 2672 sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?"); 2673 return sb.toString(); 2674 } 2675 2676 private int autoAdvanceToUiValue(int autoAdvance) { 2677 switch(autoAdvance) { 2678 case Preferences.AUTO_ADVANCE_OLDER: 2679 return UIProvider.AutoAdvance.OLDER; 2680 case Preferences.AUTO_ADVANCE_NEWER: 2681 return UIProvider.AutoAdvance.NEWER; 2682 case Preferences.AUTO_ADVANCE_MESSAGE_LIST: 2683 default: 2684 return UIProvider.AutoAdvance.LIST; 2685 } 2686 } 2687 2688 private int textZoomToUiValue(int textZoom) { 2689 switch(textZoom) { 2690 case Preferences.TEXT_ZOOM_HUGE: 2691 return UIProvider.MessageTextSize.HUGE; 2692 case Preferences.TEXT_ZOOM_LARGE: 2693 return UIProvider.MessageTextSize.LARGE; 2694 case Preferences.TEXT_ZOOM_NORMAL: 2695 return UIProvider.MessageTextSize.NORMAL; 2696 case Preferences.TEXT_ZOOM_SMALL: 2697 return UIProvider.MessageTextSize.SMALL; 2698 case Preferences.TEXT_ZOOM_TINY: 2699 return UIProvider.MessageTextSize.TINY; 2700 default: 2701 return UIProvider.MessageTextSize.NORMAL; 2702 } 2703 } 2704 2705 /** 2706 * Generate a Uri string for a combined mailbox uri 2707 * @param type the uri command type (e.g. "uimessages") 2708 * @param id the id of the item (e.g. an account, mailbox, or message id) 2709 * @return a Uri string 2710 */ 2711 private static String combinedUriString(String type, String id) { 2712 return "content://" + EmailContent.AUTHORITY + "/" + type + "/" + id; 2713 } 2714 2715 private static final long COMBINED_ACCOUNT_ID = 0x10000000; 2716 2717 /** 2718 * Generate an id for a combined mailbox of a given type 2719 * @param type the mailbox type for the combined mailbox 2720 * @return the id, as a String 2721 */ 2722 private static String combinedMailboxId(int type) { 2723 return Long.toString(Account.ACCOUNT_ID_COMBINED_VIEW + type); 2724 } 2725 2726 private static String getVirtualMailboxIdString(long accountId, int type) { 2727 return Long.toString(getVirtualMailboxId(accountId, type)); 2728 } 2729 2730 private static long getVirtualMailboxId(long accountId, int type) { 2731 return (accountId << 32) + type; 2732 } 2733 2734 private static boolean isVirtualMailbox(long mailboxId) { 2735 return mailboxId >= 0x100000000L; 2736 } 2737 2738 private static boolean isCombinedMailbox(long mailboxId) { 2739 return (mailboxId >> 32) == COMBINED_ACCOUNT_ID; 2740 } 2741 2742 private static long getVirtualMailboxAccountId(long mailboxId) { 2743 return mailboxId >> 32; 2744 } 2745 2746 private static String getVirtualMailboxAccountIdString(long mailboxId) { 2747 return Long.toString(mailboxId >> 32); 2748 } 2749 2750 private static int getVirtualMailboxType(long mailboxId) { 2751 return (int)(mailboxId & 0xF); 2752 } 2753 2754 private void addCombinedAccountRow(MatrixCursor mc) { 2755 long id = Account.getDefaultAccountId(getContext()); 2756 if (id == Account.NO_ACCOUNT) return; 2757 String idString = Long.toString(id); 2758 Object[] values = new Object[UIProvider.ACCOUNTS_PROJECTION.length]; 2759 values[UIProvider.ACCOUNT_ID_COLUMN] = 0; 2760 values[UIProvider.ACCOUNT_CAPABILITIES_COLUMN] = 2761 AccountCapabilities.UNDO | AccountCapabilities.SENDING_UNAVAILABLE; 2762 values[UIProvider.ACCOUNT_FOLDER_LIST_URI_COLUMN] = 2763 combinedUriString("uifolders", COMBINED_ACCOUNT_ID_STRING); 2764 values[UIProvider.ACCOUNT_NAME_COLUMN] = getContext().getString( 2765 R.string.mailbox_list_account_selector_combined_view); 2766 values[UIProvider.ACCOUNT_SAVE_DRAFT_URI_COLUMN] = 2767 combinedUriString("uisavedraft", idString); 2768 values[UIProvider.ACCOUNT_SEND_MESSAGE_URI_COLUMN] = 2769 combinedUriString("uisendmail", idString); 2770 values[UIProvider.ACCOUNT_UNDO_URI_COLUMN] = 2771 "'content://" + UIProvider.AUTHORITY + "/uiundo'"; 2772 values[UIProvider.ACCOUNT_URI_COLUMN] = 2773 combinedUriString("uiaccount", COMBINED_ACCOUNT_ID_STRING); 2774 values[UIProvider.ACCOUNT_MIME_TYPE_COLUMN] = EMAIL_APP_MIME_TYPE; 2775 values[UIProvider.ACCOUNT_SETTINGS_INTENT_URI_COLUMN] = 2776 getExternalUriString("settings", COMBINED_ACCOUNT_ID_STRING); 2777 values[UIProvider.ACCOUNT_COMPOSE_INTENT_URI_COLUMN] = 2778 getExternalUriStringEmail2("compose", Long.toString(id)); 2779 2780 // TODO: Get these from default account? 2781 Preferences prefs = Preferences.getPreferences(getContext()); 2782 values[UIProvider.ACCOUNT_SETTINGS_AUTO_ADVANCE_COLUMN] = 2783 Integer.toString(UIProvider.AutoAdvance.NEWER); 2784 values[UIProvider.ACCOUNT_SETTINGS_MESSAGE_TEXT_SIZE_COLUMN] = 2785 Integer.toString(UIProvider.MessageTextSize.NORMAL); 2786 values[UIProvider.ACCOUNT_SETTINGS_SNAP_HEADERS_COLUMN] = 2787 Integer.toString(UIProvider.SnapHeaderValue.ALWAYS); 2788 //.add(UIProvider.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE) 2789 values[UIProvider.ACCOUNT_SETTINGS_REPLY_BEHAVIOR_COLUMN] = 2790 Integer.toString(UIProvider.DefaultReplyBehavior.REPLY); 2791 values[UIProvider.ACCOUNT_SETTINGS_HIDE_CHECKBOXES_COLUMN] = 0; 2792 values[UIProvider.ACCOUNT_SETTINGS_CONFIRM_DELETE_COLUMN] = 2793 prefs.getConfirmDelete() ? 1 : 0; 2794 values[UIProvider.ACCOUNT_SETTINGS_CONFIRM_ARCHIVE_COLUMN] = 0; 2795 values[UIProvider.ACCOUNT_SETTINGS_CONFIRM_SEND_COLUMN] = prefs.getConfirmSend() ? 1 : 0; 2796 values[UIProvider.ACCOUNT_SETTINGS_HIDE_CHECKBOXES_COLUMN] = 2797 prefs.getHideCheckboxes() ? 1 : 0; 2798 values[UIProvider.ACCOUNT_SETTINGS_DEFAULT_INBOX_COLUMN] = combinedUriString("uifolder", 2799 combinedMailboxId(Mailbox.TYPE_INBOX)); 2800 2801 mc.addRow(values); 2802 } 2803 2804 private Cursor getVirtualMailboxCursor(long mailboxId) { 2805 MatrixCursor mc = new MatrixCursor(UIProvider.FOLDERS_PROJECTION, 1); 2806 mc.addRow(getVirtualMailboxRow(getVirtualMailboxAccountId(mailboxId), 2807 getVirtualMailboxType(mailboxId))); 2808 return mc; 2809 } 2810 2811 private Object[] getVirtualMailboxRow(long accountId, int mailboxType) { 2812 String idString = getVirtualMailboxIdString(accountId, mailboxType); 2813 Object[] values = new Object[UIProvider.FOLDERS_PROJECTION.length]; 2814 values[UIProvider.FOLDER_ID_COLUMN] = 0; 2815 values[UIProvider.FOLDER_URI_COLUMN] = combinedUriString("uifolder", idString); 2816 values[UIProvider.FOLDER_NAME_COLUMN] = getMailboxNameForType(mailboxType); 2817 values[UIProvider.FOLDER_HAS_CHILDREN_COLUMN] = 0; 2818 values[UIProvider.FOLDER_CAPABILITIES_COLUMN] = UIProvider.FolderCapabilities.IS_VIRTUAL; 2819 values[UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN] = combinedUriString("uimessages", 2820 idString); 2821 values[UIProvider.FOLDER_ID_COLUMN] = 0; 2822 return values; 2823 } 2824 2825 private Cursor uiAccounts(String[] uiProjection) { 2826 Context context = getContext(); 2827 SQLiteDatabase db = getDatabase(context); 2828 Cursor accountIdCursor = 2829 db.rawQuery("select _id from " + Account.TABLE_NAME, new String[0]); 2830 int numAccounts = accountIdCursor.getCount(); 2831 boolean combinedAccount = false; 2832 if (numAccounts > 1) { 2833 combinedAccount = true; 2834 numAccounts++; 2835 } 2836 final Bundle extras = new Bundle(); 2837 // Email always returns the accurate number of accounts 2838 extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, 1); 2839 final MatrixCursor mc = 2840 new MatrixCursorWithExtra(uiProjection, accountIdCursor.getCount(), extras); 2841 Object[] values = new Object[uiProjection.length]; 2842 try { 2843 if (combinedAccount) { 2844 addCombinedAccountRow(mc); 2845 } 2846 while (accountIdCursor.moveToNext()) { 2847 String id = accountIdCursor.getString(0); 2848 Cursor accountCursor = 2849 db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); 2850 if (accountCursor.moveToNext()) { 2851 for (int i = 0; i < uiProjection.length; i++) { 2852 values[i] = accountCursor.getString(i); 2853 } 2854 mc.addRow(values); 2855 } 2856 accountCursor.close(); 2857 } 2858 } finally { 2859 accountIdCursor.close(); 2860 } 2861 mc.setNotificationUri(context.getContentResolver(), UIPROVIDER_ACCOUNTS_NOTIFIER); 2862 return mc; 2863 } 2864 2865 /** 2866 * Generate the "attachment list" SQLite query, given a projection from UnifiedEmail 2867 * 2868 * @param uiProjection as passed from UnifiedEmail 2869 * @return the SQLite query to be executed on the EmailProvider database 2870 */ 2871 private String genQueryAttachments(String[] uiProjection) { 2872 StringBuilder sb = genSelect(sAttachmentMap, uiProjection); 2873 sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.MESSAGE_KEY + 2874 " =? "); 2875 return sb.toString(); 2876 } 2877 2878 /** 2879 * Generate the "single attachment" SQLite query, given a projection from UnifiedEmail 2880 * 2881 * @param uiProjection as passed from UnifiedEmail 2882 * @return the SQLite query to be executed on the EmailProvider database 2883 */ 2884 private String genQueryAttachment(String[] uiProjection) { 2885 StringBuilder sb = genSelect(sAttachmentMap, uiProjection); 2886 sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.ID + " =? "); 2887 return sb.toString(); 2888 } 2889 2890 /** 2891 * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail 2892 * 2893 * @param uiProjection as passed from UnifiedEmail 2894 * @return the SQLite query to be executed on the EmailProvider database 2895 */ 2896 private String genQuerySubfolders(String[] uiProjection) { 2897 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 2898 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY + 2899 " =? ORDER BY "); 2900 sb.append(MAILBOX_ORDER_BY); 2901 return sb.toString(); 2902 } 2903 2904 private static final String COMBINED_ACCOUNT_ID_STRING = Long.toString(COMBINED_ACCOUNT_ID); 2905 2906 /** 2907 * Returns a cursor over all the folders for a specific URI which corresponds to a single 2908 * account. 2909 * @param uri 2910 * @param uiProjection 2911 * @return 2912 */ 2913 private Cursor uiFolders(Uri uri, String[] uiProjection) { 2914 Context context = getContext(); 2915 SQLiteDatabase db = getDatabase(context); 2916 String id = uri.getPathSegments().get(1); 2917 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 2918 MatrixCursor mc = new MatrixCursor(UIProvider.FOLDERS_PROJECTION, 2); 2919 Object[] row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX); 2920 int numUnread = EmailContent.count(context, Message.CONTENT_URI, 2921 MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID + 2922 " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + 2923 "=" + Mailbox.TYPE_INBOX + ") AND " + MessageColumns.FLAG_READ + "=0", null); 2924 row[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = numUnread; 2925 mc.addRow(row); 2926 int numStarred = EmailContent.count(context, Message.CONTENT_URI, 2927 MessageColumns.FLAG_FAVORITE + "=1", null); 2928 if (numStarred > 0) { 2929 row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_STARRED); 2930 row[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = numStarred; 2931 mc.addRow(row); 2932 } 2933 return mc; 2934 } else { 2935 Cursor c = db.rawQuery(genQueryAccountMailboxes(uiProjection), new String[] {id}); 2936 int numStarred = EmailContent.count(context, Message.CONTENT_URI, 2937 MessageColumns.ACCOUNT_KEY + "=? AND " + MessageColumns.FLAG_FAVORITE + "=1", 2938 new String[] {id}); 2939 if (numStarred == 0) { 2940 return c; 2941 } else { 2942 // Add starred virtual folder to the cursor 2943 // Show number of messages as unread count (for backward compatibility) 2944 MatrixCursor starCursor = new MatrixCursor(uiProjection, 1); 2945 Object[] row = getVirtualMailboxRow(Long.parseLong(id), Mailbox.TYPE_STARRED); 2946 row[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = numStarred; 2947 row[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_menu_star_holo_light; 2948 starCursor.addRow(row); 2949 Cursor[] cursors = new Cursor[] {starCursor, c}; 2950 return new MergeCursor(cursors); 2951 } 2952 } 2953 } 2954 2955 /** 2956 * Returns an array of the default recent folders for a given URI which is unique for an 2957 * account. Some accounts might not have default recent folders, in which case an empty array 2958 * is returned. 2959 * @param id 2960 * @return 2961 */ 2962 private Uri[] defaultRecentFolders(final String id) { 2963 final SQLiteDatabase db = getDatabase(getContext()); 2964 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 2965 // We don't have default recents for the combined view. 2966 return new Uri[0]; 2967 } 2968 // We search for the types we want, and find corresponding IDs. 2969 final String[] idAndType = { BaseColumns._ID, UIProvider.FolderColumns.TYPE }; 2970 2971 // Sent, Drafts, and Starred are the default recents. 2972 final StringBuilder sb = genSelect(sFolderListMap, idAndType); 2973 sb.append(" FROM " + Mailbox.TABLE_NAME 2974 + " WHERE " + MailboxColumns.ACCOUNT_KEY + " = " + id 2975 + " AND " 2976 + MailboxColumns.TYPE + " IN (" + Mailbox.TYPE_SENT + 2977 ", " + Mailbox.TYPE_DRAFTS + 2978 ", " + Mailbox.TYPE_STARRED 2979 + ")"); 2980 LogUtils.d(TAG, "defaultRecentFolders: Query is %s", sb); 2981 final Cursor c = db.rawQuery(sb.toString(), null); 2982 if (c == null || c.getCount() <= 0 || !c.moveToFirst()) { 2983 return new Uri[0]; 2984 } 2985 // Read all the IDs of the mailboxes, and turn them into URIs. 2986 final Uri[] recentFolders = new Uri[c.getCount()]; 2987 int i = 0; 2988 do { 2989 final long folderId = c.getLong(0); 2990 recentFolders[i] = uiUri("uifolder", folderId); 2991 LogUtils.d(TAG, "Default recent folder: %d, with uri %s", folderId, recentFolders[i]); 2992 ++i; 2993 } while (c.moveToNext()); 2994 return recentFolders; 2995 } 2996 2997 /** 2998 * Wrapper that handles the visibility feature (i.e. the conversation list is visible, so 2999 * any pending notifications for the corresponding mailbox should be canceled) 3000 */ 3001 static class VisibilityCursor extends CursorWrapper { 3002 private final long mMailboxId; 3003 private final Context mContext; 3004 3005 public VisibilityCursor(Context context, Cursor cursor, long mailboxId) { 3006 super(cursor); 3007 mMailboxId = mailboxId; 3008 mContext = context; 3009 } 3010 3011 @Override 3012 public Bundle respond(Bundle params) { 3013 final String setVisibilityKey = 3014 UIProvider.ConversationCursorCommand.COMMAND_KEY_SET_VISIBILITY; 3015 if (params.containsKey(setVisibilityKey)) { 3016 final boolean visible = params.getBoolean(setVisibilityKey); 3017 if (visible) { 3018 NotificationController.getInstance(mContext).cancelNewMessageNotification( 3019 mMailboxId); 3020 } 3021 } 3022 // Return success 3023 Bundle response = new Bundle(); 3024 response.putString(setVisibilityKey, 3025 UIProvider.ConversationCursorCommand.COMMAND_RESPONSE_OK); 3026 return response; 3027 } 3028 } 3029 3030 /** 3031 * Handle UnifiedEmail queries here (dispatched from query()) 3032 * 3033 * @param match the UriMatcher match for the original uri passed in from UnifiedEmail 3034 * @param uri the original uri passed in from UnifiedEmail 3035 * @param uiProjection the projection passed in from UnifiedEmail 3036 * @return the result Cursor 3037 */ 3038 private Cursor uiQuery(int match, Uri uri, String[] uiProjection) { 3039 Context context = getContext(); 3040 ContentResolver resolver = context.getContentResolver(); 3041 SQLiteDatabase db = getDatabase(context); 3042 // Should we ever return null, or throw an exception?? 3043 Cursor c = null; 3044 String id = uri.getPathSegments().get(1); 3045 Uri notifyUri = null; 3046 switch(match) { 3047 case UI_ALL_FOLDERS: 3048 c = db.rawQuery(genQueryAccountAllMailboxes(uiProjection), new String[] {id}); 3049 break; 3050 case UI_RECENT_FOLDERS: 3051 c = db.rawQuery(genQueryRecentMailboxes(uiProjection), new String[] {id}); 3052 notifyUri = UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build(); 3053 break; 3054 case UI_SUBFOLDERS: 3055 c = db.rawQuery(genQuerySubfolders(uiProjection), new String[] {id}); 3056 break; 3057 case UI_MESSAGES: 3058 long mailboxId = Long.parseLong(id); 3059 if (isVirtualMailbox(mailboxId)) { 3060 c = getVirtualMailboxMessagesCursor(db, uiProjection, mailboxId); 3061 } else { 3062 c = db.rawQuery(genQueryMailboxMessages(uiProjection), new String[] {id}); 3063 } 3064 notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build(); 3065 c = new VisibilityCursor(context, c, mailboxId); 3066 break; 3067 case UI_MESSAGE: 3068 MessageQuery qq = genQueryViewMessage(uiProjection, id); 3069 String sql = qq.query; 3070 String attJson = qq.attachmentJson; 3071 // With attachments, we have another argument to bind 3072 if (attJson != null) { 3073 c = db.rawQuery(sql, new String[] {attJson, id}); 3074 } else { 3075 c = db.rawQuery(sql, new String[] {id}); 3076 } 3077 break; 3078 case UI_ATTACHMENTS: 3079 c = db.rawQuery(genQueryAttachments(uiProjection), new String[] {id}); 3080 notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build(); 3081 break; 3082 case UI_ATTACHMENT: 3083 c = db.rawQuery(genQueryAttachment(uiProjection), new String[] {id}); 3084 notifyUri = UIPROVIDER_ATTACHMENT_NOTIFIER.buildUpon().appendPath(id).build(); 3085 break; 3086 case UI_FOLDER: 3087 mailboxId = Long.parseLong(id); 3088 if (isVirtualMailbox(mailboxId)) { 3089 c = getVirtualMailboxCursor(mailboxId); 3090 } else { 3091 c = db.rawQuery(genQueryMailbox(uiProjection, id), new String[] {id}); 3092 notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(id).build(); 3093 } 3094 break; 3095 case UI_ACCOUNT: 3096 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 3097 MatrixCursor mc = new MatrixCursor(UIProvider.ACCOUNTS_PROJECTION, 1); 3098 addCombinedAccountRow(mc); 3099 c = mc; 3100 } else { 3101 c = db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); 3102 } 3103 notifyUri = UIPROVIDER_ACCOUNT_NOTIFIER.buildUpon().appendPath(id).build(); 3104 break; 3105 case UI_CONVERSATION: 3106 c = db.rawQuery(genQueryConversation(uiProjection), new String[] {id}); 3107 break; 3108 } 3109 if (notifyUri != null) { 3110 c.setNotificationUri(resolver, notifyUri); 3111 } 3112 return c; 3113 } 3114 3115 /** 3116 * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need 3117 * a few of the fields 3118 * @param uiAtt the UIProvider attachment to convert 3119 * @return the EmailProvider attachment 3120 */ 3121 private Attachment convertUiAttachmentToAttachment( 3122 com.android.mail.providers.Attachment uiAtt) { 3123 Attachment att = new Attachment(); 3124 att.mContentUri = uiAtt.contentUri.toString(); 3125 att.mFileName = uiAtt.name; 3126 att.mMimeType = uiAtt.contentType; 3127 att.mSize = uiAtt.size; 3128 return att; 3129 } 3130 3131 private String getMailboxNameForType(int mailboxType) { 3132 Context context = getContext(); 3133 int resId; 3134 switch (mailboxType) { 3135 case Mailbox.TYPE_INBOX: 3136 resId = R.string.mailbox_name_server_inbox; 3137 break; 3138 case Mailbox.TYPE_OUTBOX: 3139 resId = R.string.mailbox_name_server_outbox; 3140 break; 3141 case Mailbox.TYPE_DRAFTS: 3142 resId = R.string.mailbox_name_server_drafts; 3143 break; 3144 case Mailbox.TYPE_TRASH: 3145 resId = R.string.mailbox_name_server_trash; 3146 break; 3147 case Mailbox.TYPE_SENT: 3148 resId = R.string.mailbox_name_server_sent; 3149 break; 3150 case Mailbox.TYPE_JUNK: 3151 resId = R.string.mailbox_name_server_junk; 3152 break; 3153 case Mailbox.TYPE_STARRED: 3154 resId = R.string.widget_starred; 3155 break; 3156 default: 3157 throw new IllegalArgumentException("Illegal mailbox type"); 3158 } 3159 return context.getString(resId); 3160 } 3161 3162 /** 3163 * Create a mailbox given the account and mailboxType. 3164 */ 3165 private Mailbox createMailbox(long accountId, int mailboxType) { 3166 Context context = getContext(); 3167 Mailbox box = Mailbox.newSystemMailbox(accountId, mailboxType, 3168 getMailboxNameForType(mailboxType)); 3169 // Make sure drafts and save will show up in recents... 3170 // If these already exist (from old Email app), they will have touch times 3171 switch (mailboxType) { 3172 case Mailbox.TYPE_DRAFTS: 3173 box.mLastTouchedTime = Mailbox.DRAFTS_DEFAULT_TOUCH_TIME; 3174 break; 3175 case Mailbox.TYPE_SENT: 3176 box.mLastTouchedTime = Mailbox.SENT_DEFAULT_TOUCH_TIME; 3177 break; 3178 } 3179 box.save(context); 3180 return box; 3181 } 3182 3183 /** 3184 * Given an account name and a mailbox type, return that mailbox, creating it if necessary 3185 * @param accountName the account name to use 3186 * @param mailboxType the type of mailbox we're trying to find 3187 * @return the mailbox of the given type for the account in the uri, or null if not found 3188 */ 3189 private Mailbox getMailboxByAccountIdAndType(String accountId, int mailboxType) { 3190 long id = Long.parseLong(accountId); 3191 Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), id, mailboxType); 3192 if (mailbox == null) { 3193 mailbox = createMailbox(id, mailboxType); 3194 } 3195 return mailbox; 3196 } 3197 3198 private Message getMessageFromPathSegments(List<String> pathSegments) { 3199 Message msg = null; 3200 if (pathSegments.size() > 2) { 3201 msg = Message.restoreMessageWithId(getContext(), Long.parseLong(pathSegments.get(2))); 3202 } 3203 if (msg == null) { 3204 msg = new Message(); 3205 } 3206 return msg; 3207 } 3208 /** 3209 * Given a mailbox and the content values for a message, create/save the message in the mailbox 3210 * @param mailbox the mailbox to use 3211 * @param values the content values that represent message fields 3212 * @return the uri of the newly created message 3213 */ 3214 private Uri uiSaveMessage(Message msg, Mailbox mailbox, ContentValues values) { 3215 Context context = getContext(); 3216 // Fill in the message 3217 Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey); 3218 if (account == null) return null; 3219 msg.mFrom = account.mEmailAddress; 3220 msg.mTimeStamp = System.currentTimeMillis(); 3221 msg.mTo = values.getAsString(UIProvider.MessageColumns.TO); 3222 msg.mCc = values.getAsString(UIProvider.MessageColumns.CC); 3223 msg.mBcc = values.getAsString(UIProvider.MessageColumns.BCC); 3224 msg.mSubject = values.getAsString(UIProvider.MessageColumns.SUBJECT); 3225 msg.mText = values.getAsString(UIProvider.MessageColumns.BODY_TEXT); 3226 msg.mHtml = values.getAsString(UIProvider.MessageColumns.BODY_HTML); 3227 msg.mMailboxKey = mailbox.mId; 3228 msg.mAccountKey = mailbox.mAccountKey; 3229 msg.mDisplayName = msg.mTo; 3230 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 3231 msg.mFlagRead = true; 3232 Integer quoteStartPos = values.getAsInteger(UIProvider.MessageColumns.QUOTE_START_POS); 3233 msg.mQuotedTextStartPos = quoteStartPos == null ? 0 : quoteStartPos; 3234 int flags = 0; 3235 int draftType = values.getAsInteger(UIProvider.MessageColumns.DRAFT_TYPE); 3236 switch(draftType) { 3237 case DraftType.FORWARD: 3238 flags |= Message.FLAG_TYPE_FORWARD; 3239 break; 3240 case DraftType.REPLY_ALL: 3241 flags |= Message.FLAG_TYPE_REPLY_ALL; 3242 // Fall through 3243 case DraftType.REPLY: 3244 flags |= Message.FLAG_TYPE_REPLY; 3245 break; 3246 case DraftType.COMPOSE: 3247 flags |= Message.FLAG_TYPE_ORIGINAL; 3248 break; 3249 } 3250 msg.mFlags = flags; 3251 int draftInfo = 0; 3252 if (values.containsKey(UIProvider.MessageColumns.QUOTE_START_POS)) { 3253 draftInfo = values.getAsInteger(UIProvider.MessageColumns.QUOTE_START_POS); 3254 if (values.getAsInteger(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT) != 0) { 3255 draftInfo |= Message.DRAFT_INFO_APPEND_REF_MESSAGE; 3256 } 3257 } 3258 msg.mDraftInfo = draftInfo; 3259 String ref = values.getAsString(UIProvider.MessageColumns.REF_MESSAGE_ID); 3260 if (ref != null && msg.mQuotedTextStartPos > 0) { 3261 String refId = Uri.parse(ref).getLastPathSegment(); 3262 try { 3263 long sourceKey = Long.parseLong(refId); 3264 msg.mSourceKey = sourceKey; 3265 } catch (NumberFormatException e) { 3266 // This will be zero; the default 3267 } 3268 } 3269 3270 // Get attachments from the ContentValues 3271 List<com.android.mail.providers.Attachment> uiAtts = 3272 com.android.mail.providers.Attachment.fromJSONArray( 3273 values.getAsString(UIProvider.MessageColumns.JOINED_ATTACHMENT_INFOS)); 3274 ArrayList<Attachment> atts = new ArrayList<Attachment>(); 3275 boolean hasUnloadedAttachments = false; 3276 for (com.android.mail.providers.Attachment uiAtt: uiAtts) { 3277 Uri attUri = uiAtt.uri; 3278 if (attUri != null && attUri.getAuthority().equals(EmailContent.AUTHORITY)) { 3279 // If it's one of ours, retrieve the attachment and add it to the list 3280 long attId = Long.parseLong(attUri.getLastPathSegment()); 3281 Attachment att = Attachment.restoreAttachmentWithId(context, attId); 3282 if (att != null) { 3283 // We must clone the attachment into a new one for this message; easiest to 3284 // use a parcel here 3285 Parcel p = Parcel.obtain(); 3286 att.writeToParcel(p, 0); 3287 p.setDataPosition(0); 3288 Attachment attClone = new Attachment(p); 3289 p.recycle(); 3290 // Clear the messageKey (this is going to be a new attachment) 3291 attClone.mMessageKey = 0; 3292 // If we're sending this, it's not loaded, and we're not smart forwarding 3293 // add the download flag, so that ADS will start up 3294 if (mailbox.mType == Mailbox.TYPE_OUTBOX && att.mContentUri == null && 3295 ((account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) { 3296 attClone.mFlags |= Attachment.FLAG_DOWNLOAD_FORWARD; 3297 hasUnloadedAttachments = true; 3298 } 3299 atts.add(attClone); 3300 } 3301 } else { 3302 // Convert external attachment to one of ours and add to the list 3303 atts.add(convertUiAttachmentToAttachment(uiAtt)); 3304 } 3305 } 3306 if (!atts.isEmpty()) { 3307 msg.mAttachments = atts; 3308 msg.mFlagAttachment = true; 3309 if (hasUnloadedAttachments) { 3310 Utility.showToast(context, R.string.message_view_attachment_background_load); 3311 } 3312 } 3313 // Save it or update it... 3314 if (!msg.isSaved()) { 3315 msg.save(context); 3316 } else { 3317 // This is tricky due to how messages/attachments are saved; rather than putz with 3318 // what's changed, we'll delete/re-add them 3319 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 3320 // Delete all existing attachments 3321 ops.add(ContentProviderOperation.newDelete( 3322 ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId)) 3323 .build()); 3324 // Delete the body 3325 ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI) 3326 .withSelection(Body.MESSAGE_KEY + "=?", new String[] {Long.toString(msg.mId)}) 3327 .build()); 3328 // Add the ops for the message, atts, and body 3329 msg.addSaveOps(ops); 3330 // Do it! 3331 try { 3332 applyBatch(ops); 3333 } catch (OperationApplicationException e) { 3334 } 3335 } 3336 if (mailbox.mType == Mailbox.TYPE_OUTBOX) { 3337 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 3338 mServiceCallback, mailbox.mAccountKey); 3339 try { 3340 service.startSync(mailbox.mId, true); 3341 } catch (RemoteException e) { 3342 } 3343 long originalMsgId = msg.mSourceKey; 3344 if (originalMsgId != 0) { 3345 Message originalMsg = Message.restoreMessageWithId(context, originalMsgId); 3346 // If the original message exists, set its forwarded/replied to flags 3347 if (originalMsg != null) { 3348 ContentValues cv = new ContentValues(); 3349 flags = originalMsg.mFlags; 3350 switch(draftType) { 3351 case DraftType.FORWARD: 3352 flags |= Message.FLAG_FORWARDED; 3353 break; 3354 case DraftType.REPLY_ALL: 3355 case DraftType.REPLY: 3356 flags |= Message.FLAG_REPLIED_TO; 3357 break; 3358 } 3359 cv.put(Message.FLAGS, flags); 3360 context.getContentResolver().update(ContentUris.withAppendedId( 3361 Message.CONTENT_URI, originalMsgId), cv, null, null); 3362 } 3363 } 3364 } 3365 return uiUri("uimessage", msg.mId); 3366 } 3367 3368 /** 3369 * Create and send the message via the account indicated in the uri 3370 * @param uri the incoming uri 3371 * @param values the content values that represent message fields 3372 * @return the uri of the created message 3373 */ 3374 private Uri uiSendMail(Uri uri, ContentValues values) { 3375 List<String> pathSegments = uri.getPathSegments(); 3376 Mailbox mailbox = getMailboxByAccountIdAndType(pathSegments.get(1), Mailbox.TYPE_OUTBOX); 3377 if (mailbox == null) return null; 3378 Message msg = getMessageFromPathSegments(pathSegments); 3379 try { 3380 return uiSaveMessage(msg, mailbox, values); 3381 } finally { 3382 // Kick observers 3383 getContext().getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 3384 } 3385 } 3386 3387 /** 3388 * Create a message and save it to the drafts folder of the account indicated in the uri 3389 * @param uri the incoming uri 3390 * @param values the content values that represent message fields 3391 * @return the uri of the created message 3392 */ 3393 private Uri uiSaveDraft(Uri uri, ContentValues values) { 3394 List<String> pathSegments = uri.getPathSegments(); 3395 Mailbox mailbox = getMailboxByAccountIdAndType(pathSegments.get(1), Mailbox.TYPE_DRAFTS); 3396 if (mailbox == null) return null; 3397 Message msg = getMessageFromPathSegments(pathSegments); 3398 return uiSaveMessage(msg, mailbox, values); 3399 } 3400 3401 private int uiUpdateDraft(Uri uri, ContentValues values) { 3402 Context context = getContext(); 3403 Message msg = Message.restoreMessageWithId(context, 3404 Long.parseLong(uri.getPathSegments().get(1))); 3405 if (msg == null) return 0; 3406 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 3407 if (mailbox == null) return 0; 3408 uiSaveMessage(msg, mailbox, values); 3409 return 1; 3410 } 3411 3412 private int uiSendDraft(Uri uri, ContentValues values) { 3413 Context context = getContext(); 3414 Message msg = Message.restoreMessageWithId(context, 3415 Long.parseLong(uri.getPathSegments().get(1))); 3416 if (msg == null) return 0; 3417 long mailboxId = Mailbox.findMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_OUTBOX); 3418 if (mailboxId == Mailbox.NO_MAILBOX) return 0; 3419 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 3420 if (mailbox == null) return 0; 3421 uiSaveMessage(msg, mailbox, values); 3422 // Kick observers 3423 context.getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 3424 return 1; 3425 } 3426 3427 private void putIntegerLongOrBoolean(ContentValues values, String columnName, Object value) { 3428 if (value instanceof Integer) { 3429 Integer intValue = (Integer)value; 3430 values.put(columnName, intValue); 3431 } else if (value instanceof Boolean) { 3432 Boolean boolValue = (Boolean)value; 3433 values.put(columnName, boolValue ? 1 : 0); 3434 } else if (value instanceof Long) { 3435 Long longValue = (Long)value; 3436 values.put(columnName, longValue); 3437 } 3438 } 3439 3440 /** 3441 * Update the timestamps for the folders specified and notifies on the recent folder URI. 3442 * @param folders 3443 * @return number of folders updated 3444 */ 3445 private int updateTimestamp(final Context context, String id, Uri[] folders){ 3446 int updated = 0; 3447 final long now = System.currentTimeMillis(); 3448 final ContentResolver resolver = context.getContentResolver(); 3449 final ContentValues touchValues = new ContentValues(); 3450 for (int i=0, size=folders.length; i < size; ++i) { 3451 touchValues.put(MailboxColumns.LAST_TOUCHED_TIME, now); 3452 LogUtils.d(TAG, "updateStamp: %s updated", folders[i]); 3453 updated += resolver.update(folders[i], touchValues, null, null); 3454 } 3455 final Uri toNotify = 3456 UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build(); 3457 LogUtils.d(TAG, "updateTimestamp: Notifying on %s", toNotify); 3458 resolver.notifyChange(toNotify, null); 3459 return updated; 3460 } 3461 3462 /** 3463 * Updates the recent folders. The values to be updated are specified as ContentValues pairs 3464 * of (Folder URI, access timestamp). Returns nonzero if successful, always. 3465 * @param uri 3466 * @param values 3467 * @return nonzero value always. 3468 */ 3469 private int uiUpdateRecentFolders(Uri uri, ContentValues values) { 3470 final int numFolders = values.size(); 3471 final String id = uri.getPathSegments().get(1); 3472 final Uri[] folders = new Uri[numFolders]; 3473 final Context context = getContext(); 3474 final NotificationController controller = NotificationController.getInstance(context); 3475 int i = 0; 3476 for (final String uriString: values.keySet()) { 3477 folders[i] = Uri.parse(uriString); 3478 try { 3479 final String mailboxIdString = folders[i].getLastPathSegment(); 3480 final long mailboxId = Long.parseLong(mailboxIdString); 3481 controller.cancelNewMessageNotification(mailboxId); 3482 } catch (NumberFormatException e) { 3483 // Keep on going... 3484 } 3485 } 3486 return updateTimestamp(context, id, folders); 3487 } 3488 3489 /** 3490 * Populates the recent folders according to the design. 3491 * @param uri 3492 * @return the number of recent folders were populated. 3493 */ 3494 private int uiPopulateRecentFolders(Uri uri) { 3495 final Context context = getContext(); 3496 final String id = uri.getLastPathSegment(); 3497 final Uri[] recentFolders = defaultRecentFolders(id); 3498 final int numFolders = recentFolders.length; 3499 if (numFolders <= 0) { 3500 return 0; 3501 } 3502 final int rowsUpdated = updateTimestamp(context, id, recentFolders); 3503 LogUtils.d(TAG, "uiPopulateRecentFolders: %d folders changed", rowsUpdated); 3504 return rowsUpdated; 3505 } 3506 3507 private int uiUpdateAttachment(Uri uri, ContentValues uiValues) { 3508 Integer stateValue = uiValues.getAsInteger(UIProvider.AttachmentColumns.STATE); 3509 if (stateValue != null) { 3510 // This is a command from UIProvider 3511 long attachmentId = Long.parseLong(uri.getLastPathSegment()); 3512 Context context = getContext(); 3513 Attachment attachment = 3514 Attachment.restoreAttachmentWithId(context, attachmentId); 3515 if (attachment == null) { 3516 // Went away; ah, well... 3517 return 0; 3518 } 3519 ContentValues values = new ContentValues(); 3520 switch (stateValue.intValue()) { 3521 case UIProvider.AttachmentState.NOT_SAVED: 3522 // Set state, try to cancel request 3523 values.put(AttachmentColumns.UI_STATE, stateValue); 3524 values.put(AttachmentColumns.FLAGS, 3525 attachment.mFlags &= ~Attachment.FLAG_DOWNLOAD_USER_REQUEST); 3526 attachment.update(context, values); 3527 return 1; 3528 case UIProvider.AttachmentState.DOWNLOADING: 3529 // Set state and destination; request download 3530 values.put(AttachmentColumns.UI_STATE, stateValue); 3531 Integer destinationValue = 3532 uiValues.getAsInteger(UIProvider.AttachmentColumns.DESTINATION); 3533 values.put(AttachmentColumns.UI_DESTINATION, 3534 destinationValue == null ? 0 : destinationValue); 3535 values.put(AttachmentColumns.FLAGS, 3536 attachment.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST); 3537 attachment.update(context, values); 3538 return 1; 3539 case UIProvider.AttachmentState.SAVED: 3540 // If this is an inline attachment, notify message has changed 3541 if (!TextUtils.isEmpty(attachment.mContentId)) { 3542 notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, attachment.mMessageKey); 3543 } 3544 return 1; 3545 } 3546 } 3547 return 0; 3548 } 3549 3550 private int uiUpdateFolder(Uri uri, ContentValues uiValues) { 3551 Uri ourUri = convertToEmailProviderUri(uri, Mailbox.CONTENT_URI, true); 3552 if (ourUri == null) return 0; 3553 ContentValues ourValues = new ContentValues(); 3554 // This should only be called via update to "recent folders" 3555 for (String columnName: uiValues.keySet()) { 3556 if (columnName.equals(MailboxColumns.LAST_TOUCHED_TIME)) { 3557 ourValues.put(MailboxColumns.LAST_TOUCHED_TIME, uiValues.getAsLong(columnName)); 3558 } 3559 } 3560 return update(ourUri, ourValues, null, null); 3561 } 3562 3563 private ContentValues convertUiMessageValues(Message message, ContentValues values) { 3564 ContentValues ourValues = new ContentValues(); 3565 for (String columnName: values.keySet()) { 3566 Object val = values.get(columnName); 3567 if (columnName.equals(UIProvider.ConversationColumns.STARRED)) { 3568 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val); 3569 } else if (columnName.equals(UIProvider.ConversationColumns.READ)) { 3570 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val); 3571 } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 3572 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val); 3573 } else if (columnName.equals(UIProvider.ConversationColumns.FOLDER_LIST)) { 3574 // Convert from folder list uri to mailbox key 3575 Uri uri = Uri.parse((String)val); 3576 Long mailboxId = Long.parseLong(uri.getLastPathSegment()); 3577 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId); 3578 } else if (columnName.equals(UIProvider.ConversationColumns.RAW_FOLDERS)) { 3579 // Ignore; this is updated by the FOLDER_LIST update above. 3580 } else if (columnName.equals(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES)) { 3581 Address[] fromList = Address.unpack(message.mFrom); 3582 Preferences prefs = Preferences.getPreferences(getContext()); 3583 for (Address sender : fromList) { 3584 String email = sender.getAddress(); 3585 prefs.setSenderAsTrusted(email); 3586 } 3587 } else { 3588 throw new IllegalArgumentException("Can't update " + columnName + " in message"); 3589 } 3590 } 3591 return ourValues; 3592 } 3593 3594 private Uri convertToEmailProviderUri(Uri uri, Uri newBaseUri, boolean asProvider) { 3595 String idString = uri.getLastPathSegment(); 3596 try { 3597 long id = Long.parseLong(idString); 3598 Uri ourUri = ContentUris.withAppendedId(newBaseUri, id); 3599 if (asProvider) { 3600 ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build(); 3601 } 3602 return ourUri; 3603 } catch (NumberFormatException e) { 3604 return null; 3605 } 3606 } 3607 3608 private Message getMessageFromLastSegment(Uri uri) { 3609 long messageId = Long.parseLong(uri.getLastPathSegment()); 3610 return Message.restoreMessageWithId(getContext(), messageId); 3611 } 3612 3613 /** 3614 * Add an undo operation for the current sequence; if the sequence is newer than what we've had, 3615 * clear out the undo list and start over 3616 * @param uri the uri we're working on 3617 * @param op the ContentProviderOperation to perform upon undo 3618 */ 3619 private void addToSequence(Uri uri, ContentProviderOperation op) { 3620 String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER); 3621 if (sequenceString != null) { 3622 int sequence = Integer.parseInt(sequenceString); 3623 if (sequence > mLastSequence) { 3624 // Reset sequence 3625 mLastSequenceOps.clear(); 3626 mLastSequence = sequence; 3627 } 3628 // TODO: Need something to indicate a change isn't ready (undoable) 3629 mLastSequenceOps.add(op); 3630 } 3631 } 3632 3633 // TODO: This should depend on flags on the mailbox... 3634 private boolean uploadsToServer(Context context, Mailbox m) { 3635 if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX || 3636 m.mType == Mailbox.TYPE_SEARCH) { 3637 return false; 3638 } 3639 String protocol = Account.getProtocol(context, m.mAccountKey); 3640 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 3641 return (info != null && info.syncChanges); 3642 } 3643 3644 private int uiUpdateMessage(Uri uri, ContentValues values) { 3645 Context context = getContext(); 3646 Message msg = getMessageFromLastSegment(uri); 3647 if (msg == null) return 0; 3648 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 3649 if (mailbox == null) return 0; 3650 Uri ourBaseUri = uploadsToServer(context, mailbox) ? Message.SYNCED_CONTENT_URI : 3651 Message.CONTENT_URI; 3652 Uri ourUri = convertToEmailProviderUri(uri, ourBaseUri, true); 3653 if (ourUri == null) return 0; 3654 3655 // Special case - meeting response 3656 if (values.containsKey(UIProvider.MessageOperations.RESPOND_COLUMN)) { 3657 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 3658 mServiceCallback, mailbox.mAccountKey); 3659 try { 3660 service.sendMeetingResponse(msg.mId, 3661 values.getAsInteger(UIProvider.MessageOperations.RESPOND_COLUMN)); 3662 // Delete the message immediately 3663 uiDeleteMessage(uri); 3664 Utility.showToast(context, R.string.confirm_response); 3665 // Notify box has changed so the deletion is reflected in the UI 3666 notifyUIConversationMailbox(mailbox.mId); 3667 } catch (RemoteException e) { 3668 } 3669 return 1; 3670 } 3671 3672 ContentValues undoValues = new ContentValues(); 3673 ContentValues ourValues = convertUiMessageValues(msg, values); 3674 for (String columnName: ourValues.keySet()) { 3675 if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 3676 undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey); 3677 } else if (columnName.equals(MessageColumns.FLAG_READ)) { 3678 undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead); 3679 } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) { 3680 undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite); 3681 } 3682 } 3683 if (undoValues == null || undoValues.size() == 0) { 3684 return -1; 3685 } 3686 ContentProviderOperation op = 3687 ContentProviderOperation.newUpdate(convertToEmailProviderUri( 3688 uri, ourBaseUri, false)) 3689 .withValues(undoValues) 3690 .build(); 3691 addToSequence(uri, op); 3692 return update(ourUri, ourValues, null, null); 3693 } 3694 3695 private int uiDeleteMessage(Uri uri) { 3696 Context context = getContext(); 3697 Message msg = getMessageFromLastSegment(uri); 3698 if (msg == null) return 0; 3699 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 3700 if (mailbox == null) return 0; 3701 if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_DRAFTS) { 3702 // We actually delete these, including attachments 3703 AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId); 3704 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, mailbox.mId); 3705 return context.getContentResolver().delete( 3706 ContentUris.withAppendedId(Message.CONTENT_URI, msg.mId), null, null); 3707 } 3708 Mailbox trashMailbox = 3709 Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH); 3710 if (trashMailbox == null) return 0; 3711 ContentValues values = new ContentValues(); 3712 values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId); 3713 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, mailbox.mId); 3714 return uiUpdateMessage(uri, values); 3715 } 3716 3717 private Cursor uiUndo(String[] projection) { 3718 // First see if we have any operations saved 3719 // TODO: Make sure seq matches 3720 if (!mLastSequenceOps.isEmpty()) { 3721 try { 3722 // TODO Always use this projection? Or what's passed in? 3723 // Not sure if UI wants it, but I'm making a cursor of convo uri's 3724 MatrixCursor c = new MatrixCursor( 3725 new String[] {UIProvider.ConversationColumns.URI}, 3726 mLastSequenceOps.size()); 3727 for (ContentProviderOperation op: mLastSequenceOps) { 3728 c.addRow(new String[] {op.getUri().toString()}); 3729 } 3730 // Just apply the batch and we're done! 3731 applyBatch(mLastSequenceOps); 3732 // But clear the operations 3733 mLastSequenceOps.clear(); 3734 // Tell the UI there are changes 3735 ContentResolver resolver = getContext().getContentResolver(); 3736 resolver.notifyChange(UIPROVIDER_CONVERSATION_NOTIFIER, null); 3737 resolver.notifyChange(UIPROVIDER_FOLDER_NOTIFIER, null); 3738 return c; 3739 } catch (OperationApplicationException e) { 3740 } 3741 } 3742 return new MatrixCursor(projection, 0); 3743 } 3744 3745 private void notifyUIConversation(Uri uri) { 3746 String id = uri.getLastPathSegment(); 3747 Message msg = Message.restoreMessageWithId(getContext(), Long.parseLong(id)); 3748 if (msg != null) { 3749 notifyUIConversationMailbox(msg.mMailboxKey); 3750 } 3751 } 3752 3753 /** 3754 * Notify about the Mailbox id passed in 3755 * @param id the Mailbox id to be notified 3756 */ 3757 private void notifyUIConversationMailbox(long id) { 3758 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(id)); 3759 Mailbox mailbox = Mailbox.restoreMailboxWithId(getContext(), id); 3760 // Notify combined inbox... 3761 if (mailbox.mType == Mailbox.TYPE_INBOX) { 3762 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, 3763 EmailProvider.combinedMailboxId(Mailbox.TYPE_INBOX)); 3764 } 3765 notifyWidgets(id); 3766 } 3767 3768 private void notifyUI(Uri uri, String id) { 3769 Uri notifyUri = uri.buildUpon().appendPath(id).build(); 3770 getContext().getContentResolver().notifyChange(notifyUri, null); 3771 } 3772 3773 private void notifyUI(Uri uri, long id) { 3774 notifyUI(uri, Long.toString(id)); 3775 } 3776 3777 /** 3778 * Support for services and service notifications 3779 */ 3780 3781 private final IEmailServiceCallback.Stub mServiceCallback = 3782 new IEmailServiceCallback.Stub() { 3783 3784 @Override 3785 public void syncMailboxListStatus(long accountId, int statusCode, int progress) 3786 throws RemoteException { 3787 } 3788 3789 @Override 3790 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) 3791 throws RemoteException { 3792 // We'll get callbacks here from the services, which we'll pass back to the UI 3793 Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, mailboxId); 3794 EmailProvider.this.getContext().getContentResolver().notifyChange(uri, null); 3795 } 3796 3797 @Override 3798 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 3799 int progress) throws RemoteException { 3800 } 3801 3802 @Override 3803 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) 3804 throws RemoteException { 3805 } 3806 3807 @Override 3808 public void loadMessageStatus(long messageId, int statusCode, int progress) 3809 throws RemoteException { 3810 } 3811 }; 3812 3813 private Cursor uiFolderRefresh(Uri uri) { 3814 Context context = getContext(); 3815 String idString = uri.getLastPathSegment(); 3816 long id = Long.parseLong(idString); 3817 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, id); 3818 if (mailbox == null) return null; 3819 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 3820 mServiceCallback, mailbox.mAccountKey); 3821 try { 3822 service.startSync(id, true); 3823 } catch (RemoteException e) { 3824 } 3825 return null; 3826 } 3827 3828 //Number of additional messages to load when a user selects "Load more..." in POP/IMAP boxes 3829 public static final int VISIBLE_LIMIT_INCREMENT = 10; 3830 //Number of additional messages to load when a user selects "Load more..." in a search 3831 public static final int SEARCH_MORE_INCREMENT = 10; 3832 3833 private Cursor uiFolderLoadMore(Uri uri) { 3834 Context context = getContext(); 3835 String idString = uri.getLastPathSegment(); 3836 long id = Long.parseLong(idString); 3837 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, id); 3838 if (mailbox == null) return null; 3839 if (mailbox.mType == Mailbox.TYPE_SEARCH) { 3840 // Ask for 10 more messages 3841 mSearchParams.mOffset += SEARCH_MORE_INCREMENT; 3842 runSearchQuery(context, mailbox.mAccountKey, id); 3843 } else { 3844 ContentValues values = new ContentValues(); 3845 values.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT); 3846 values.put(EmailContent.ADD_COLUMN_NAME, VISIBLE_LIMIT_INCREMENT); 3847 Uri mailboxUri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, id); 3848 // Increase the limit 3849 context.getContentResolver().update(mailboxUri, values, null, null); 3850 // And order a refresh 3851 uiFolderRefresh(uri); 3852 } 3853 return null; 3854 } 3855 3856 private static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__"; 3857 private SearchParams mSearchParams; 3858 3859 /** 3860 * Returns the search mailbox for the specified account, creating one if necessary 3861 * @return the search mailbox for the passed in account 3862 */ 3863 private Mailbox getSearchMailbox(long accountId) { 3864 Context context = getContext(); 3865 Mailbox m = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SEARCH); 3866 if (m == null) { 3867 m = new Mailbox(); 3868 m.mAccountKey = accountId; 3869 m.mServerId = SEARCH_MAILBOX_SERVER_ID; 3870 m.mFlagVisible = false; 3871 m.mDisplayName = SEARCH_MAILBOX_SERVER_ID; 3872 m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 3873 m.mType = Mailbox.TYPE_SEARCH; 3874 m.mFlags = Mailbox.FLAG_HOLDS_MAIL; 3875 m.mParentKey = Mailbox.NO_MAILBOX; 3876 m.save(context); 3877 } 3878 return m; 3879 } 3880 3881 private void runSearchQuery(final Context context, final long accountId, 3882 final long searchMailboxId) { 3883 // Start the search running in the background 3884 new Thread(new Runnable() { 3885 @Override 3886 public void run() { 3887 try { 3888 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 3889 mServiceCallback, accountId); 3890 if (service != null) { 3891 try { 3892 // Save away the total count 3893 mSearchParams.mTotalCount = service.searchMessages(accountId, 3894 mSearchParams, searchMailboxId); 3895 //Log.d(TAG, "TotalCount to UI: " + mSearchParams.mTotalCount); 3896 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, searchMailboxId); 3897 } catch (RemoteException e) { 3898 Log.e("searchMessages", "RemoteException", e); 3899 } 3900 } 3901 } finally { 3902 } 3903 }}).start(); 3904 3905 } 3906 3907 // TODO: Handle searching for more... 3908 private Cursor uiSearch(Uri uri, String[] projection) { 3909 final long accountId = Long.parseLong(uri.getLastPathSegment()); 3910 3911 // TODO: Check the actual mailbox 3912 Mailbox inbox = Mailbox.restoreMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX); 3913 if (inbox == null) return null; 3914 3915 String filter = uri.getQueryParameter(UIProvider.SearchQueryParameters.QUERY); 3916 if (filter == null) { 3917 throw new IllegalArgumentException("No query parameter in search query"); 3918 } 3919 3920 // Find/create our search mailbox 3921 Mailbox searchMailbox = getSearchMailbox(accountId); 3922 final long searchMailboxId = searchMailbox.mId; 3923 3924 mSearchParams = new SearchParams(inbox.mId, filter, searchMailboxId); 3925 3926 final Context context = getContext(); 3927 if (mSearchParams.mOffset == 0) { 3928 // Delete existing contents of search mailbox 3929 ContentResolver resolver = context.getContentResolver(); 3930 resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId, 3931 null); 3932 ContentValues cv = new ContentValues(); 3933 // For now, use the actual query as the name of the mailbox 3934 cv.put(Mailbox.DISPLAY_NAME, mSearchParams.mFilter); 3935 resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), 3936 cv, null, null); 3937 } 3938 3939 // Start the search running in the background 3940 runSearchQuery(context, accountId, searchMailboxId); 3941 3942 // This will look just like a "normal" folder 3943 return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI, 3944 searchMailbox.mId), projection); 3945 } 3946 3947 private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; 3948 private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION = 3949 MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" + 3950 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX; 3951 private static final String MESSAGES_FOR_ACCOUNT_SELECTION = MessageColumns.ACCOUNT_KEY + "=?"; 3952 3953 /** 3954 * Delete an account and clean it up 3955 */ 3956 private int uiDeleteAccount(Uri uri) { 3957 Context context = getContext(); 3958 long accountId = Long.parseLong(uri.getLastPathSegment()); 3959 try { 3960 // Get the account URI. 3961 final Account account = Account.restoreAccountWithId(context, accountId); 3962 if (account == null) { 3963 return 0; // Already deleted? 3964 } 3965 3966 deleteAccountData(context, accountId); 3967 3968 // Now delete the account itself 3969 uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 3970 context.getContentResolver().delete(uri, null, null); 3971 3972 // Clean up 3973 AccountBackupRestore.backup(context); 3974 SecurityPolicy.getInstance(context).reducePolicies(); 3975 MailActivityEmail.setServicesEnabledSync(context); 3976 return 1; 3977 } catch (Exception e) { 3978 Log.w(Logging.LOG_TAG, "Exception while deleting account", e); 3979 } 3980 return 0; 3981 } 3982 3983 private int uiDeleteAccountData(Uri uri) { 3984 Context context = getContext(); 3985 long accountId = Long.parseLong(uri.getLastPathSegment()); 3986 // Get the account URI. 3987 final Account account = Account.restoreAccountWithId(context, accountId); 3988 if (account == null) { 3989 return 0; // Already deleted? 3990 } 3991 deleteAccountData(context, accountId); 3992 return 1; 3993 } 3994 3995 private void deleteAccountData(Context context, long accountId) { 3996 // Delete synced attachments 3997 AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId); 3998 3999 // Delete synced email, leaving only an empty inbox. We do this in two phases: 4000 // 1. Delete all non-inbox mailboxes (which will delete all of their messages) 4001 // 2. Delete all remaining messages (which will be the inbox messages) 4002 ContentResolver resolver = context.getContentResolver(); 4003 String[] accountIdArgs = new String[] { Long.toString(accountId) }; 4004 resolver.delete(Mailbox.CONTENT_URI, 4005 MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION, 4006 accountIdArgs); 4007 resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs); 4008 4009 // Delete sync keys on remaining items 4010 ContentValues cv = new ContentValues(); 4011 cv.putNull(Account.SYNC_KEY); 4012 resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs); 4013 cv.clear(); 4014 cv.putNull(Mailbox.SYNC_KEY); 4015 resolver.update(Mailbox.CONTENT_URI, cv, 4016 MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs); 4017 4018 // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable 4019 IEmailService service = EmailServiceUtils.getServiceForAccount(context, null, accountId); 4020 if (service != null) { 4021 try { 4022 service.deleteAccountPIMData(accountId); 4023 } catch (RemoteException e) { 4024 // Can't do anything about this 4025 } 4026 } 4027 } 4028 4029 private int[] mSavedWidgetIds = new int[0]; 4030 private ArrayList<Long> mWidgetNotifyMailboxes = new ArrayList<Long>(); 4031 private AppWidgetManager mAppWidgetManager; 4032 private ComponentName mEmailComponent; 4033 4034 private void notifyWidgets(long mailboxId) { 4035 Context context = getContext(); 4036 // Lazily initialize these 4037 if (mAppWidgetManager == null) { 4038 mAppWidgetManager = AppWidgetManager.getInstance(context); 4039 mEmailComponent = new ComponentName(context, WidgetProvider.PROVIDER_NAME); 4040 } 4041 4042 // See if we have to populate our array of mailboxes used in widgets 4043 int[] widgetIds = mAppWidgetManager.getAppWidgetIds(mEmailComponent); 4044 if (!Arrays.equals(widgetIds, mSavedWidgetIds)) { 4045 mSavedWidgetIds = widgetIds; 4046 String[][] widgetInfos = BaseWidgetProvider.getWidgetInfo(context, widgetIds); 4047 // widgetInfo now has pairs of account uri/folder uri 4048 mWidgetNotifyMailboxes.clear(); 4049 for (String[] widgetInfo: widgetInfos) { 4050 try { 4051 if (widgetInfo == null) continue; 4052 long id = Long.parseLong(Uri.parse(widgetInfo[1]).getLastPathSegment()); 4053 if (!isCombinedMailbox(id)) { 4054 // For a regular mailbox, just add it to the list 4055 if (!mWidgetNotifyMailboxes.contains(id)) { 4056 mWidgetNotifyMailboxes.add(id); 4057 } 4058 } else { 4059 switch (getVirtualMailboxType(id)) { 4060 // We only handle the combined inbox in widgets 4061 case Mailbox.TYPE_INBOX: 4062 Cursor c = query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION, 4063 MailboxColumns.TYPE + "=?", 4064 new String[] {Integer.toString(Mailbox.TYPE_INBOX)}, null); 4065 try { 4066 while (c.moveToNext()) { 4067 mWidgetNotifyMailboxes.add( 4068 c.getLong(Mailbox.ID_PROJECTION_COLUMN)); 4069 } 4070 } finally { 4071 c.close(); 4072 } 4073 break; 4074 } 4075 } 4076 } catch (NumberFormatException e) { 4077 // Move along 4078 } 4079 } 4080 } 4081 4082 // If our mailbox needs to be notified, do so... 4083 if (mWidgetNotifyMailboxes.contains(mailboxId)) { 4084 Intent intent = new Intent(Utils.ACTION_NOTIFY_DATASET_CHANGED); 4085 intent.putExtra(Utils.EXTRA_FOLDER_URI, uiUri("uifolder", mailboxId)); 4086 intent.setType(EMAIL_APP_MIME_TYPE); 4087 context.sendBroadcast(intent); 4088 } 4089 } 4090} 4091