EmailProvider.java revision f419287f22ae44f25e1ba1f757ec33c7941bbfa8
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 * Generate the "view message" SQLite query, given a projection from UnifiedEmail 2340 * 2341 * @param uiProjection as passed from UnifiedEmail 2342 * @return the SQLite query to be executed on the EmailProvider database 2343 */ 2344 private String genQueryViewMessage(String[] uiProjection, String id) { 2345 Context context = getContext(); 2346 long messageId = Long.parseLong(id); 2347 Message msg = Message.restoreMessageWithId(context, messageId); 2348 ContentValues values = new ContentValues(); 2349 if (msg != null) { 2350 if (msg.mFlagLoaded == Message.FLAG_LOADED_PARTIAL) { 2351 EmailServiceProxy service = 2352 EmailServiceUtils.getServiceForAccount(context, null, msg.mAccountKey); 2353 try { 2354 service.loadMore(messageId); 2355 } catch (RemoteException e) { 2356 // Nothing to do 2357 } 2358 } 2359 Body body = Body.restoreBodyWithMessageId(context, messageId); 2360 if (body != null) { 2361 if (body.mHtmlContent != null) { 2362 if (IMG_TAG_START_REGEX.matcher(body.mHtmlContent).find()) { 2363 values.put(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, 1); 2364 } 2365 } 2366 } 2367 Address[] fromList = Address.unpack(msg.mFrom); 2368 int autoShowImages = 0; 2369 Preferences prefs = Preferences.getPreferences(context); 2370 for (Address sender : fromList) { 2371 String email = sender.getAddress(); 2372 if (prefs.shouldShowImagesFor(email)) { 2373 autoShowImages = 1; 2374 break; 2375 } 2376 } 2377 values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, autoShowImages); 2378 // Add attachments... 2379 Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId); 2380 if (atts.length > 0) { 2381 ArrayList<com.android.mail.providers.Attachment> uiAtts = 2382 new ArrayList<com.android.mail.providers.Attachment>(); 2383 for (Attachment att : atts) { 2384 if (att.mContentId != null && att.mContentUri != null) { 2385 continue; 2386 } 2387 com.android.mail.providers.Attachment uiAtt = 2388 new com.android.mail.providers.Attachment(); 2389 uiAtt.name = att.mFileName; 2390 uiAtt.contentType = att.mMimeType; 2391 uiAtt.size = (int) att.mSize; 2392 uiAtt.uri = uiUri("uiattachment", att.mId); 2393 uiAtts.add(uiAtt); 2394 } 2395 values.put(UIProvider.MessageColumns.ATTACHMENTS, 2396 com.android.mail.providers.Attachment.toJSONArray(uiAtts)); 2397 } 2398 if (msg.mDraftInfo != 0) { 2399 values.put(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, 2400 (msg.mDraftInfo & Message.DRAFT_INFO_APPEND_REF_MESSAGE) != 0 ? 1 : 0); 2401 values.put(UIProvider.MessageColumns.QUOTE_START_POS, 2402 msg.mDraftInfo & Message.DRAFT_INFO_QUOTE_POS_MASK); 2403 } 2404 } 2405 if ((msg.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) { 2406 values.put(UIProvider.MessageColumns.EVENT_INTENT_URI, 2407 "content://ui.email2.android.com/event/" + msg.mId); 2408 } 2409 StringBuilder sb = genSelect(sMessageViewMap, uiProjection, values); 2410 sb.append(" FROM " + Message.TABLE_NAME + "," + Body.TABLE_NAME + " WHERE " + 2411 Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " AND " + 2412 Message.TABLE_NAME + "." + Message.RECORD_ID + "=?"); 2413 return sb.toString(); 2414 } 2415 2416 /** 2417 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 2418 * 2419 * @param uiProjection as passed from UnifiedEmail 2420 * @return the SQLite query to be executed on the EmailProvider database 2421 */ 2422 private String genQueryMailboxMessages(String[] uiProjection) { 2423 StringBuilder sb = genSelect(sMessageListMap, uiProjection); 2424 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.MAILBOX_KEY + "=? ORDER BY " + 2425 MessageColumns.TIMESTAMP + " DESC"); 2426 return sb.toString(); 2427 } 2428 2429 /** 2430 * Generate various virtual mailbox SQLite queries, given a projection from UnifiedEmail 2431 * 2432 * @param uiProjection as passed from UnifiedEmail 2433 * @param id the id of the virtual mailbox 2434 * @return the SQLite query to be executed on the EmailProvider database 2435 */ 2436 private Cursor getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection, 2437 long mailboxId) { 2438 ContentValues values = new ContentValues(); 2439 values.put(UIProvider.ConversationColumns.COLOR, CONVERSATION_COLOR); 2440 StringBuilder sb = genSelect(sMessageListMap, uiProjection, values); 2441 if (isCombinedMailbox(mailboxId)) { 2442 switch (getVirtualMailboxType(mailboxId)) { 2443 case Mailbox.TYPE_INBOX: 2444 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2445 MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID + 2446 " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + 2447 "=" + Mailbox.TYPE_INBOX + ") ORDER BY " + MessageColumns.TIMESTAMP + 2448 " DESC"); 2449 break; 2450 case Mailbox.TYPE_STARRED: 2451 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2452 MessageColumns.FLAG_FAVORITE + "=1 ORDER BY " + 2453 MessageColumns.TIMESTAMP + " DESC"); 2454 break; 2455 default: 2456 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); 2457 } 2458 return db.rawQuery(sb.toString(), null); 2459 } else { 2460 switch (getVirtualMailboxType(mailboxId)) { 2461 case Mailbox.TYPE_STARRED: 2462 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2463 MessageColumns.ACCOUNT_KEY + "=? AND " + 2464 MessageColumns.FLAG_FAVORITE + "=1 ORDER BY " + 2465 MessageColumns.TIMESTAMP + " DESC"); 2466 break; 2467 default: 2468 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); 2469 } 2470 return db.rawQuery(sb.toString(), 2471 new String[] {getVirtualMailboxAccountIdString(mailboxId)}); 2472 } 2473 } 2474 2475 /** 2476 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 2477 * 2478 * @param uiProjection as passed from UnifiedEmail 2479 * @return the SQLite query to be executed on the EmailProvider database 2480 */ 2481 private String genQueryConversation(String[] uiProjection) { 2482 StringBuilder sb = genSelect(sMessageListMap, uiProjection); 2483 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.RECORD_ID + "=?"); 2484 return sb.toString(); 2485 } 2486 2487 /** 2488 * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail 2489 * 2490 * @param uiProjection as passed from UnifiedEmail 2491 * @return the SQLite query to be executed on the EmailProvider database 2492 */ 2493 private String genQueryAccountMailboxes(String[] uiProjection) { 2494 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 2495 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2496 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2497 " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY "); 2498 sb.append(MAILBOX_ORDER_BY); 2499 return sb.toString(); 2500 } 2501 2502 /** 2503 * Generate the "all folders" SQLite query, given a projection from UnifiedEmail. The list is 2504 * sorted by the name as it appears in a hierarchical listing 2505 * 2506 * @param uiProjection as passed from UnifiedEmail 2507 * @return the SQLite query to be executed on the EmailProvider database 2508 */ 2509 private String genQueryAccountAllMailboxes(String[] uiProjection) { 2510 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 2511 // Use a derived column to choose either hierarchicalName or displayName 2512 sb.append(", case when " + MailboxColumns.HIERARCHICAL_NAME + " is null then " + 2513 MailboxColumns.DISPLAY_NAME + " else " + MailboxColumns.HIERARCHICAL_NAME + 2514 " end as h_name"); 2515 // Order by the derived column 2516 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2517 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2518 " ORDER BY h_name"); 2519 return sb.toString(); 2520 } 2521 2522 /** 2523 * Generate the "recent folder list" SQLite query, given a projection from UnifiedEmail 2524 * 2525 * @param uiProjection as passed from UnifiedEmail 2526 * @return the SQLite query to be executed on the EmailProvider database 2527 */ 2528 private String genQueryRecentMailboxes(String[] uiProjection) { 2529 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 2530 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2531 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2532 " AND " + MailboxColumns.PARENT_KEY + " < 0 AND " + 2533 MailboxColumns.LAST_TOUCHED_TIME + " > 0 ORDER BY " + 2534 MailboxColumns.LAST_TOUCHED_TIME + " DESC"); 2535 return sb.toString(); 2536 } 2537 2538 /** 2539 * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail 2540 * 2541 * @param uiProjection as passed from UnifiedEmail 2542 * @return the SQLite query to be executed on the EmailProvider database 2543 */ 2544 private String genQueryMailbox(String[] uiProjection, String id) { 2545 long mailboxId = Long.parseLong(id); 2546 ContentValues values = new ContentValues(); 2547 if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) { 2548 // This is the current search mailbox; use the total count 2549 values = new ContentValues(); 2550 values.put(UIProvider.FolderColumns.TOTAL_COUNT, mSearchParams.mTotalCount); 2551 // "load more" is valid for search results 2552 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 2553 uiUriString("uiloadmore", mailboxId)); 2554 } else { 2555 Context context = getContext(); 2556 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 2557 // Make sure we can't get NPE if mailbox has disappeared (the result will end up moot) 2558 if (mailbox != null) { 2559 String protocol = Account.getProtocol(context, mailbox.mAccountKey); 2560 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 2561 // "load more" is valid for protocols not supporting "lookback" 2562 if (info != null && !info.offerLookback) { 2563 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 2564 uiUriString("uiloadmore", mailboxId)); 2565 } else { 2566 int caps = UIProvider.FolderCapabilities.SUPPORTS_SETTINGS; 2567 if ((mailbox.mFlags & Mailbox.FLAG_ACCEPTS_MOVED_MAIL) != 0) { 2568 caps |= UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES; 2569 } 2570 values.put(UIProvider.FolderColumns.CAPABILITIES, caps); 2571 } 2572 // For trash, we don't allow undo 2573 if (mailbox.mType == Mailbox.TYPE_TRASH) { 2574 values.put(UIProvider.FolderColumns.CAPABILITIES, 2575 UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES | 2576 UIProvider.FolderCapabilities.CAN_HOLD_MAIL | 2577 UIProvider.FolderCapabilities.DELETE_ACTION_FINAL); 2578 } 2579 if (isVirtualMailbox(mailboxId)) { 2580 int capa = values.getAsInteger(UIProvider.FolderColumns.CAPABILITIES); 2581 values.put(UIProvider.FolderColumns.CAPABILITIES, 2582 capa | UIProvider.FolderCapabilities.IS_VIRTUAL); 2583 } 2584 } 2585 } 2586 StringBuilder sb = genSelect(sFolderListMap, uiProjection, values); 2587 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ID + "=?"); 2588 return sb.toString(); 2589 } 2590 2591 private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://ui.email.android.com"); 2592 2593 private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com"); 2594 2595 private static String getExternalUriString(String segment, String account) { 2596 return BASE_EXTERNAL_URI.buildUpon().appendPath(segment) 2597 .appendQueryParameter("account", account).build().toString(); 2598 } 2599 2600 private static String getExternalUriStringEmail2(String segment, String account) { 2601 return BASE_EXTERAL_URI2.buildUpon().appendPath(segment) 2602 .appendQueryParameter("account", account).build().toString(); 2603 } 2604 2605 /** 2606 * Generate a "single account" SQLite query, given a projection from UnifiedEmail 2607 * 2608 * @param uiProjection as passed from UnifiedEmail 2609 * @return the SQLite query to be executed on the EmailProvider database 2610 */ 2611 private String genQueryAccount(String[] uiProjection, String id) { 2612 ContentValues values = new ContentValues(); 2613 long accountId = Long.parseLong(id); 2614 2615 // Get account capabilities from the service 2616 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(getContext(), 2617 mServiceCallback, accountId); 2618 int capabilities = 0; 2619 try { 2620 capabilities = service.getCapabilities(accountId); 2621 } catch (RemoteException e) { 2622 } 2623 values.put(UIProvider.AccountColumns.CAPABILITIES, capabilities); 2624 2625 values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI, 2626 getExternalUriString("settings", id)); 2627 values.put(UIProvider.AccountColumns.COMPOSE_URI, 2628 getExternalUriStringEmail2("compose", id)); 2629 values.put(UIProvider.AccountColumns.MIME_TYPE, EMAIL_APP_MIME_TYPE); 2630 values.put(UIProvider.AccountColumns.COLOR, ACCOUNT_COLOR); 2631 2632 Preferences prefs = Preferences.getPreferences(getContext()); 2633 values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE, 2634 prefs.getConfirmDelete() ? "1" : "0"); 2635 values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND, 2636 prefs.getConfirmSend() ? "1" : "0"); 2637 values.put(UIProvider.AccountColumns.SettingsColumns.HIDE_CHECKBOXES, 2638 prefs.getHideCheckboxes() ? "1" : "0"); 2639 int autoAdvance = prefs.getAutoAdvanceDirection(); 2640 values.put(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE, 2641 autoAdvanceToUiValue(autoAdvance)); 2642 int textZoom = prefs.getTextZoom(); 2643 values.put(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE, 2644 textZoomToUiValue(textZoom)); 2645 // Set default inbox, if we've got an inbox; otherwise, say initial sync needed 2646 long mailboxId = Mailbox.findMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX); 2647 if (mailboxId != Mailbox.NO_MAILBOX) { 2648 values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX, 2649 uiUriString("uifolder", mailboxId)); 2650 values.put(UIProvider.AccountColumns.SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC); 2651 } else { 2652 values.put(UIProvider.AccountColumns.SYNC_STATUS, 2653 UIProvider.SyncStatus.INITIAL_SYNC_NEEDED); 2654 } 2655 2656 StringBuilder sb = genSelect(sAccountListMap, uiProjection, values); 2657 sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?"); 2658 return sb.toString(); 2659 } 2660 2661 private int autoAdvanceToUiValue(int autoAdvance) { 2662 switch(autoAdvance) { 2663 case Preferences.AUTO_ADVANCE_OLDER: 2664 return UIProvider.AutoAdvance.OLDER; 2665 case Preferences.AUTO_ADVANCE_NEWER: 2666 return UIProvider.AutoAdvance.NEWER; 2667 case Preferences.AUTO_ADVANCE_MESSAGE_LIST: 2668 default: 2669 return UIProvider.AutoAdvance.LIST; 2670 } 2671 } 2672 2673 private int textZoomToUiValue(int textZoom) { 2674 switch(textZoom) { 2675 case Preferences.TEXT_ZOOM_HUGE: 2676 return UIProvider.MessageTextSize.HUGE; 2677 case Preferences.TEXT_ZOOM_LARGE: 2678 return UIProvider.MessageTextSize.LARGE; 2679 case Preferences.TEXT_ZOOM_NORMAL: 2680 return UIProvider.MessageTextSize.NORMAL; 2681 case Preferences.TEXT_ZOOM_SMALL: 2682 return UIProvider.MessageTextSize.SMALL; 2683 case Preferences.TEXT_ZOOM_TINY: 2684 return UIProvider.MessageTextSize.TINY; 2685 default: 2686 return UIProvider.MessageTextSize.NORMAL; 2687 } 2688 } 2689 2690 /** 2691 * Generate a Uri string for a combined mailbox uri 2692 * @param type the uri command type (e.g. "uimessages") 2693 * @param id the id of the item (e.g. an account, mailbox, or message id) 2694 * @return a Uri string 2695 */ 2696 private static String combinedUriString(String type, String id) { 2697 return "content://" + EmailContent.AUTHORITY + "/" + type + "/" + id; 2698 } 2699 2700 private static final long COMBINED_ACCOUNT_ID = 0x10000000; 2701 2702 /** 2703 * Generate an id for a combined mailbox of a given type 2704 * @param type the mailbox type for the combined mailbox 2705 * @return the id, as a String 2706 */ 2707 private static String combinedMailboxId(int type) { 2708 return Long.toString(Account.ACCOUNT_ID_COMBINED_VIEW + type); 2709 } 2710 2711 private static String getVirtualMailboxIdString(long accountId, int type) { 2712 return Long.toString(getVirtualMailboxId(accountId, type)); 2713 } 2714 2715 private static long getVirtualMailboxId(long accountId, int type) { 2716 return (accountId << 32) + type; 2717 } 2718 2719 private static boolean isVirtualMailbox(long mailboxId) { 2720 return mailboxId >= 0x100000000L; 2721 } 2722 2723 private static boolean isCombinedMailbox(long mailboxId) { 2724 return (mailboxId >> 32) == COMBINED_ACCOUNT_ID; 2725 } 2726 2727 private static long getVirtualMailboxAccountId(long mailboxId) { 2728 return mailboxId >> 32; 2729 } 2730 2731 private static String getVirtualMailboxAccountIdString(long mailboxId) { 2732 return Long.toString(mailboxId >> 32); 2733 } 2734 2735 private static int getVirtualMailboxType(long mailboxId) { 2736 return (int)(mailboxId & 0xF); 2737 } 2738 2739 private void addCombinedAccountRow(MatrixCursor mc) { 2740 long id = Account.getDefaultAccountId(getContext()); 2741 if (id == Account.NO_ACCOUNT) return; 2742 String idString = Long.toString(id); 2743 Object[] values = new Object[UIProvider.ACCOUNTS_PROJECTION.length]; 2744 values[UIProvider.ACCOUNT_ID_COLUMN] = 0; 2745 values[UIProvider.ACCOUNT_CAPABILITIES_COLUMN] = 2746 AccountCapabilities.UNDO | AccountCapabilities.SENDING_UNAVAILABLE; 2747 values[UIProvider.ACCOUNT_FOLDER_LIST_URI_COLUMN] = 2748 combinedUriString("uifolders", COMBINED_ACCOUNT_ID_STRING); 2749 values[UIProvider.ACCOUNT_NAME_COLUMN] = getContext().getString( 2750 R.string.mailbox_list_account_selector_combined_view); 2751 values[UIProvider.ACCOUNT_SAVE_DRAFT_URI_COLUMN] = 2752 combinedUriString("uisavedraft", idString); 2753 values[UIProvider.ACCOUNT_SEND_MESSAGE_URI_COLUMN] = 2754 combinedUriString("uisendmail", idString); 2755 values[UIProvider.ACCOUNT_UNDO_URI_COLUMN] = 2756 "'content://" + UIProvider.AUTHORITY + "/uiundo'"; 2757 values[UIProvider.ACCOUNT_URI_COLUMN] = 2758 combinedUriString("uiaccount", COMBINED_ACCOUNT_ID_STRING); 2759 values[UIProvider.ACCOUNT_MIME_TYPE_COLUMN] = EMAIL_APP_MIME_TYPE; 2760 values[UIProvider.ACCOUNT_SETTINGS_INTENT_URI_COLUMN] = 2761 getExternalUriString("settings", COMBINED_ACCOUNT_ID_STRING); 2762 values[UIProvider.ACCOUNT_COMPOSE_INTENT_URI_COLUMN] = 2763 getExternalUriStringEmail2("compose", Long.toString(id)); 2764 2765 // TODO: Get these from default account? 2766 Preferences prefs = Preferences.getPreferences(getContext()); 2767 values[UIProvider.ACCOUNT_SETTINGS_AUTO_ADVANCE_COLUMN] = 2768 Integer.toString(UIProvider.AutoAdvance.NEWER); 2769 values[UIProvider.ACCOUNT_SETTINGS_MESSAGE_TEXT_SIZE_COLUMN] = 2770 Integer.toString(UIProvider.MessageTextSize.NORMAL); 2771 values[UIProvider.ACCOUNT_SETTINGS_SNAP_HEADERS_COLUMN] = 2772 Integer.toString(UIProvider.SnapHeaderValue.ALWAYS); 2773 //.add(UIProvider.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE) 2774 values[UIProvider.ACCOUNT_SETTINGS_REPLY_BEHAVIOR_COLUMN] = 2775 Integer.toString(UIProvider.DefaultReplyBehavior.REPLY); 2776 values[UIProvider.ACCOUNT_SETTINGS_HIDE_CHECKBOXES_COLUMN] = 0; 2777 values[UIProvider.ACCOUNT_SETTINGS_CONFIRM_DELETE_COLUMN] = 2778 prefs.getConfirmDelete() ? 1 : 0; 2779 values[UIProvider.ACCOUNT_SETTINGS_CONFIRM_ARCHIVE_COLUMN] = 0; 2780 values[UIProvider.ACCOUNT_SETTINGS_CONFIRM_SEND_COLUMN] = prefs.getConfirmSend() ? 1 : 0; 2781 values[UIProvider.ACCOUNT_SETTINGS_HIDE_CHECKBOXES_COLUMN] = 2782 prefs.getHideCheckboxes() ? 1 : 0; 2783 values[UIProvider.ACCOUNT_SETTINGS_DEFAULT_INBOX_COLUMN] = combinedUriString("uifolder", 2784 combinedMailboxId(Mailbox.TYPE_INBOX)); 2785 2786 mc.addRow(values); 2787 } 2788 2789 private Cursor getVirtualMailboxCursor(long mailboxId) { 2790 MatrixCursor mc = new MatrixCursor(UIProvider.FOLDERS_PROJECTION, 1); 2791 mc.addRow(getVirtualMailboxRow(getVirtualMailboxAccountId(mailboxId), 2792 getVirtualMailboxType(mailboxId))); 2793 return mc; 2794 } 2795 2796 private Object[] getVirtualMailboxRow(long accountId, int mailboxType) { 2797 String idString = getVirtualMailboxIdString(accountId, mailboxType); 2798 Object[] values = new Object[UIProvider.FOLDERS_PROJECTION.length]; 2799 values[UIProvider.FOLDER_ID_COLUMN] = 0; 2800 values[UIProvider.FOLDER_URI_COLUMN] = combinedUriString("uifolder", idString); 2801 values[UIProvider.FOLDER_NAME_COLUMN] = getMailboxNameForType(mailboxType); 2802 values[UIProvider.FOLDER_HAS_CHILDREN_COLUMN] = 0; 2803 values[UIProvider.FOLDER_CAPABILITIES_COLUMN] = UIProvider.FolderCapabilities.IS_VIRTUAL; 2804 values[UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN] = combinedUriString("uimessages", 2805 idString); 2806 values[UIProvider.FOLDER_ID_COLUMN] = 0; 2807 return values; 2808 } 2809 2810 private Cursor uiAccounts(String[] uiProjection) { 2811 Context context = getContext(); 2812 SQLiteDatabase db = getDatabase(context); 2813 Cursor accountIdCursor = 2814 db.rawQuery("select _id from " + Account.TABLE_NAME, new String[0]); 2815 int numAccounts = accountIdCursor.getCount(); 2816 boolean combinedAccount = false; 2817 if (numAccounts > 1) { 2818 combinedAccount = true; 2819 numAccounts++; 2820 } 2821 final Bundle extras = new Bundle(); 2822 // Email always returns the accurate number of accounts 2823 extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, 1); 2824 final MatrixCursor mc = 2825 new MatrixCursorWithExtra(uiProjection, accountIdCursor.getCount(), extras); 2826 Object[] values = new Object[uiProjection.length]; 2827 try { 2828 if (combinedAccount) { 2829 addCombinedAccountRow(mc); 2830 } 2831 while (accountIdCursor.moveToNext()) { 2832 String id = accountIdCursor.getString(0); 2833 Cursor accountCursor = 2834 db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); 2835 if (accountCursor.moveToNext()) { 2836 for (int i = 0; i < uiProjection.length; i++) { 2837 values[i] = accountCursor.getString(i); 2838 } 2839 mc.addRow(values); 2840 } 2841 accountCursor.close(); 2842 } 2843 } finally { 2844 accountIdCursor.close(); 2845 } 2846 mc.setNotificationUri(context.getContentResolver(), UIPROVIDER_ACCOUNTS_NOTIFIER); 2847 return mc; 2848 } 2849 2850 /** 2851 * Generate the "attachment list" SQLite query, given a projection from UnifiedEmail 2852 * 2853 * @param uiProjection as passed from UnifiedEmail 2854 * @return the SQLite query to be executed on the EmailProvider database 2855 */ 2856 private String genQueryAttachments(String[] uiProjection) { 2857 StringBuilder sb = genSelect(sAttachmentMap, uiProjection); 2858 sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.MESSAGE_KEY + 2859 " =? "); 2860 return sb.toString(); 2861 } 2862 2863 /** 2864 * Generate the "single attachment" SQLite query, given a projection from UnifiedEmail 2865 * 2866 * @param uiProjection as passed from UnifiedEmail 2867 * @return the SQLite query to be executed on the EmailProvider database 2868 */ 2869 private String genQueryAttachment(String[] uiProjection) { 2870 StringBuilder sb = genSelect(sAttachmentMap, uiProjection); 2871 sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.ID + " =? "); 2872 return sb.toString(); 2873 } 2874 2875 /** 2876 * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail 2877 * 2878 * @param uiProjection as passed from UnifiedEmail 2879 * @return the SQLite query to be executed on the EmailProvider database 2880 */ 2881 private String genQuerySubfolders(String[] uiProjection) { 2882 StringBuilder sb = genSelect(sFolderListMap, uiProjection); 2883 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY + 2884 " =? ORDER BY "); 2885 sb.append(MAILBOX_ORDER_BY); 2886 return sb.toString(); 2887 } 2888 2889 private static final String COMBINED_ACCOUNT_ID_STRING = Long.toString(COMBINED_ACCOUNT_ID); 2890 2891 /** 2892 * Returns a cursor over all the folders for a specific URI which corresponds to a single 2893 * account. 2894 * @param uri 2895 * @param uiProjection 2896 * @return 2897 */ 2898 private Cursor uiFolders(Uri uri, String[] uiProjection) { 2899 Context context = getContext(); 2900 SQLiteDatabase db = getDatabase(context); 2901 String id = uri.getPathSegments().get(1); 2902 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 2903 MatrixCursor mc = new MatrixCursor(UIProvider.FOLDERS_PROJECTION, 2); 2904 Object[] row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX); 2905 int numUnread = EmailContent.count(context, Message.CONTENT_URI, 2906 MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID + 2907 " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + 2908 "=" + Mailbox.TYPE_INBOX + ") AND " + MessageColumns.FLAG_READ + "=0", null); 2909 row[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = numUnread; 2910 mc.addRow(row); 2911 int numStarred = EmailContent.count(context, Message.CONTENT_URI, 2912 MessageColumns.FLAG_FAVORITE + "=1", null); 2913 if (numStarred > 0) { 2914 row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_STARRED); 2915 row[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = numStarred; 2916 mc.addRow(row); 2917 } 2918 return mc; 2919 } else { 2920 Cursor c = db.rawQuery(genQueryAccountMailboxes(uiProjection), new String[] {id}); 2921 int numStarred = EmailContent.count(context, Message.CONTENT_URI, 2922 MessageColumns.ACCOUNT_KEY + "=? AND " + MessageColumns.FLAG_FAVORITE + "=1", 2923 new String[] {id}); 2924 if (numStarred == 0) { 2925 return c; 2926 } else { 2927 // Add starred virtual folder to the cursor 2928 // Show number of messages as unread count (for backward compatibility) 2929 MatrixCursor starCursor = new MatrixCursor(uiProjection, 1); 2930 Object[] row = getVirtualMailboxRow(Long.parseLong(id), Mailbox.TYPE_STARRED); 2931 row[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = numStarred; 2932 row[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_menu_star_holo_light; 2933 starCursor.addRow(row); 2934 Cursor[] cursors = new Cursor[] {starCursor, c}; 2935 return new MergeCursor(cursors); 2936 } 2937 } 2938 } 2939 2940 /** 2941 * Returns an array of the default recent folders for a given URI which is unique for an 2942 * account. Some accounts might not have default recent folders, in which case an empty array 2943 * is returned. 2944 * @param id 2945 * @return 2946 */ 2947 private Uri[] defaultRecentFolders(final String id) { 2948 final SQLiteDatabase db = getDatabase(getContext()); 2949 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 2950 // We don't have default recents for the combined view. 2951 return new Uri[0]; 2952 } 2953 // We search for the types we want, and find corresponding IDs. 2954 final String[] idAndType = { BaseColumns._ID, UIProvider.FolderColumns.TYPE }; 2955 2956 // Sent, Drafts, and Starred are the default recents. 2957 final StringBuilder sb = genSelect(sFolderListMap, idAndType); 2958 sb.append(" FROM " + Mailbox.TABLE_NAME 2959 + " WHERE " + MailboxColumns.ACCOUNT_KEY + " = " + id 2960 + " AND " 2961 + MailboxColumns.TYPE + " IN (" + Mailbox.TYPE_SENT + 2962 ", " + Mailbox.TYPE_DRAFTS + 2963 ", " + Mailbox.TYPE_STARRED 2964 + ")"); 2965 LogUtils.d(TAG, "defaultRecentFolders: Query is %s", sb); 2966 final Cursor c = db.rawQuery(sb.toString(), null); 2967 if (c == null || c.getCount() <= 0 || !c.moveToFirst()) { 2968 return new Uri[0]; 2969 } 2970 // Read all the IDs of the mailboxes, and turn them into URIs. 2971 final Uri[] recentFolders = new Uri[c.getCount()]; 2972 int i = 0; 2973 do { 2974 final long folderId = c.getLong(0); 2975 recentFolders[i] = uiUri("uifolder", folderId); 2976 LogUtils.d(TAG, "Default recent folder: %d, with uri %s", folderId, recentFolders[i]); 2977 ++i; 2978 } while (c.moveToNext()); 2979 return recentFolders; 2980 } 2981 2982 /** 2983 * Wrapper that handles the visibility feature (i.e. the conversation list is visible, so 2984 * any pending notifications for the corresponding mailbox should be canceled) 2985 */ 2986 static class VisibilityCursor extends CursorWrapper { 2987 private final long mMailboxId; 2988 private final Context mContext; 2989 2990 public VisibilityCursor(Context context, Cursor cursor, long mailboxId) { 2991 super(cursor); 2992 mMailboxId = mailboxId; 2993 mContext = context; 2994 } 2995 2996 @Override 2997 public Bundle respond(Bundle params) { 2998 final String setVisibilityKey = 2999 UIProvider.ConversationCursorCommand.COMMAND_KEY_SET_VISIBILITY; 3000 if (params.containsKey(setVisibilityKey)) { 3001 final boolean visible = params.getBoolean(setVisibilityKey); 3002 if (visible) { 3003 NotificationController.getInstance(mContext).cancelNewMessageNotification( 3004 mMailboxId); 3005 } 3006 } 3007 // Return success 3008 Bundle response = new Bundle(); 3009 response.putString(setVisibilityKey, 3010 UIProvider.ConversationCursorCommand.COMMAND_RESPONSE_OK); 3011 return response; 3012 } 3013 } 3014 3015 /** 3016 * Handle UnifiedEmail queries here (dispatched from query()) 3017 * 3018 * @param match the UriMatcher match for the original uri passed in from UnifiedEmail 3019 * @param uri the original uri passed in from UnifiedEmail 3020 * @param uiProjection the projection passed in from UnifiedEmail 3021 * @return the result Cursor 3022 */ 3023 private Cursor uiQuery(int match, Uri uri, String[] uiProjection) { 3024 Context context = getContext(); 3025 ContentResolver resolver = context.getContentResolver(); 3026 SQLiteDatabase db = getDatabase(context); 3027 // Should we ever return null, or throw an exception?? 3028 Cursor c = null; 3029 String id = uri.getPathSegments().get(1); 3030 Uri notifyUri = null; 3031 switch(match) { 3032 case UI_ALL_FOLDERS: 3033 c = db.rawQuery(genQueryAccountAllMailboxes(uiProjection), new String[] {id}); 3034 break; 3035 case UI_RECENT_FOLDERS: 3036 c = db.rawQuery(genQueryRecentMailboxes(uiProjection), new String[] {id}); 3037 notifyUri = UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build(); 3038 break; 3039 case UI_SUBFOLDERS: 3040 c = db.rawQuery(genQuerySubfolders(uiProjection), new String[] {id}); 3041 break; 3042 case UI_MESSAGES: 3043 long mailboxId = Long.parseLong(id); 3044 if (isVirtualMailbox(mailboxId)) { 3045 c = getVirtualMailboxMessagesCursor(db, uiProjection, mailboxId); 3046 } else { 3047 c = db.rawQuery(genQueryMailboxMessages(uiProjection), new String[] {id}); 3048 } 3049 notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build(); 3050 c = new VisibilityCursor(context, c, mailboxId); 3051 break; 3052 case UI_MESSAGE: 3053 c = db.rawQuery(genQueryViewMessage(uiProjection, id), new String[] {id}); 3054 break; 3055 case UI_ATTACHMENTS: 3056 c = db.rawQuery(genQueryAttachments(uiProjection), new String[] {id}); 3057 notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build(); 3058 break; 3059 case UI_ATTACHMENT: 3060 c = db.rawQuery(genQueryAttachment(uiProjection), new String[] {id}); 3061 notifyUri = UIPROVIDER_ATTACHMENT_NOTIFIER.buildUpon().appendPath(id).build(); 3062 break; 3063 case UI_FOLDER: 3064 mailboxId = Long.parseLong(id); 3065 if (isVirtualMailbox(mailboxId)) { 3066 c = getVirtualMailboxCursor(mailboxId); 3067 } else { 3068 c = db.rawQuery(genQueryMailbox(uiProjection, id), new String[] {id}); 3069 notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(id).build(); 3070 } 3071 break; 3072 case UI_ACCOUNT: 3073 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 3074 MatrixCursor mc = new MatrixCursor(UIProvider.ACCOUNTS_PROJECTION, 1); 3075 addCombinedAccountRow(mc); 3076 c = mc; 3077 } else { 3078 c = db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); 3079 } 3080 notifyUri = UIPROVIDER_ACCOUNT_NOTIFIER.buildUpon().appendPath(id).build(); 3081 break; 3082 case UI_CONVERSATION: 3083 c = db.rawQuery(genQueryConversation(uiProjection), new String[] {id}); 3084 break; 3085 } 3086 if (notifyUri != null) { 3087 c.setNotificationUri(resolver, notifyUri); 3088 } 3089 return c; 3090 } 3091 3092 /** 3093 * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need 3094 * a few of the fields 3095 * @param uiAtt the UIProvider attachment to convert 3096 * @return the EmailProvider attachment 3097 */ 3098 private Attachment convertUiAttachmentToAttachment( 3099 com.android.mail.providers.Attachment uiAtt) { 3100 Attachment att = new Attachment(); 3101 att.mContentUri = uiAtt.contentUri.toString(); 3102 att.mFileName = uiAtt.name; 3103 att.mMimeType = uiAtt.contentType; 3104 att.mSize = uiAtt.size; 3105 return att; 3106 } 3107 3108 private String getMailboxNameForType(int mailboxType) { 3109 Context context = getContext(); 3110 int resId; 3111 switch (mailboxType) { 3112 case Mailbox.TYPE_INBOX: 3113 resId = R.string.mailbox_name_server_inbox; 3114 break; 3115 case Mailbox.TYPE_OUTBOX: 3116 resId = R.string.mailbox_name_server_outbox; 3117 break; 3118 case Mailbox.TYPE_DRAFTS: 3119 resId = R.string.mailbox_name_server_drafts; 3120 break; 3121 case Mailbox.TYPE_TRASH: 3122 resId = R.string.mailbox_name_server_trash; 3123 break; 3124 case Mailbox.TYPE_SENT: 3125 resId = R.string.mailbox_name_server_sent; 3126 break; 3127 case Mailbox.TYPE_JUNK: 3128 resId = R.string.mailbox_name_server_junk; 3129 break; 3130 case Mailbox.TYPE_STARRED: 3131 resId = R.string.widget_starred; 3132 break; 3133 default: 3134 throw new IllegalArgumentException("Illegal mailbox type"); 3135 } 3136 return context.getString(resId); 3137 } 3138 3139 /** 3140 * Create a mailbox given the account and mailboxType. 3141 */ 3142 private Mailbox createMailbox(long accountId, int mailboxType) { 3143 Context context = getContext(); 3144 Mailbox box = Mailbox.newSystemMailbox(accountId, mailboxType, 3145 getMailboxNameForType(mailboxType)); 3146 // Make sure drafts and save will show up in recents... 3147 // If these already exist (from old Email app), they will have touch times 3148 switch (mailboxType) { 3149 case Mailbox.TYPE_DRAFTS: 3150 box.mLastTouchedTime = Mailbox.DRAFTS_DEFAULT_TOUCH_TIME; 3151 break; 3152 case Mailbox.TYPE_SENT: 3153 box.mLastTouchedTime = Mailbox.SENT_DEFAULT_TOUCH_TIME; 3154 break; 3155 } 3156 box.save(context); 3157 return box; 3158 } 3159 3160 /** 3161 * Given an account name and a mailbox type, return that mailbox, creating it if necessary 3162 * @param accountName the account name to use 3163 * @param mailboxType the type of mailbox we're trying to find 3164 * @return the mailbox of the given type for the account in the uri, or null if not found 3165 */ 3166 private Mailbox getMailboxByAccountIdAndType(String accountId, int mailboxType) { 3167 long id = Long.parseLong(accountId); 3168 Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), id, mailboxType); 3169 if (mailbox == null) { 3170 mailbox = createMailbox(id, mailboxType); 3171 } 3172 return mailbox; 3173 } 3174 3175 private Message getMessageFromPathSegments(List<String> pathSegments) { 3176 Message msg = null; 3177 if (pathSegments.size() > 2) { 3178 msg = Message.restoreMessageWithId(getContext(), Long.parseLong(pathSegments.get(2))); 3179 } 3180 if (msg == null) { 3181 msg = new Message(); 3182 } 3183 return msg; 3184 } 3185 /** 3186 * Given a mailbox and the content values for a message, create/save the message in the mailbox 3187 * @param mailbox the mailbox to use 3188 * @param values the content values that represent message fields 3189 * @return the uri of the newly created message 3190 */ 3191 private Uri uiSaveMessage(Message msg, Mailbox mailbox, ContentValues values) { 3192 Context context = getContext(); 3193 // Fill in the message 3194 Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey); 3195 if (account == null) return null; 3196 msg.mFrom = account.mEmailAddress; 3197 msg.mTimeStamp = System.currentTimeMillis(); 3198 msg.mTo = values.getAsString(UIProvider.MessageColumns.TO); 3199 msg.mCc = values.getAsString(UIProvider.MessageColumns.CC); 3200 msg.mBcc = values.getAsString(UIProvider.MessageColumns.BCC); 3201 msg.mSubject = values.getAsString(UIProvider.MessageColumns.SUBJECT); 3202 msg.mText = values.getAsString(UIProvider.MessageColumns.BODY_TEXT); 3203 msg.mHtml = values.getAsString(UIProvider.MessageColumns.BODY_HTML); 3204 msg.mMailboxKey = mailbox.mId; 3205 msg.mAccountKey = mailbox.mAccountKey; 3206 msg.mDisplayName = msg.mTo; 3207 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 3208 msg.mFlagRead = true; 3209 Integer quoteStartPos = values.getAsInteger(UIProvider.MessageColumns.QUOTE_START_POS); 3210 msg.mQuotedTextStartPos = quoteStartPos == null ? 0 : quoteStartPos; 3211 int flags = 0; 3212 int draftType = values.getAsInteger(UIProvider.MessageColumns.DRAFT_TYPE); 3213 switch(draftType) { 3214 case DraftType.FORWARD: 3215 flags |= Message.FLAG_TYPE_FORWARD; 3216 break; 3217 case DraftType.REPLY_ALL: 3218 flags |= Message.FLAG_TYPE_REPLY_ALL; 3219 // Fall through 3220 case DraftType.REPLY: 3221 flags |= Message.FLAG_TYPE_REPLY; 3222 break; 3223 case DraftType.COMPOSE: 3224 flags |= Message.FLAG_TYPE_ORIGINAL; 3225 break; 3226 } 3227 msg.mFlags = flags; 3228 int draftInfo = 0; 3229 if (values.containsKey(UIProvider.MessageColumns.QUOTE_START_POS)) { 3230 draftInfo = values.getAsInteger(UIProvider.MessageColumns.QUOTE_START_POS); 3231 if (values.getAsInteger(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT) != 0) { 3232 draftInfo |= Message.DRAFT_INFO_APPEND_REF_MESSAGE; 3233 } 3234 } 3235 msg.mDraftInfo = draftInfo; 3236 String ref = values.getAsString(UIProvider.MessageColumns.REF_MESSAGE_ID); 3237 if (ref != null && msg.mQuotedTextStartPos > 0) { 3238 String refId = Uri.parse(ref).getLastPathSegment(); 3239 try { 3240 long sourceKey = Long.parseLong(refId); 3241 msg.mSourceKey = sourceKey; 3242 } catch (NumberFormatException e) { 3243 // This will be zero; the default 3244 } 3245 } 3246 3247 // Get attachments from the ContentValues 3248 List<com.android.mail.providers.Attachment> uiAtts = 3249 com.android.mail.providers.Attachment.fromJSONArray( 3250 values.getAsString(UIProvider.MessageColumns.JOINED_ATTACHMENT_INFOS)); 3251 ArrayList<Attachment> atts = new ArrayList<Attachment>(); 3252 boolean hasUnloadedAttachments = false; 3253 for (com.android.mail.providers.Attachment uiAtt: uiAtts) { 3254 Uri attUri = uiAtt.uri; 3255 if (attUri != null && attUri.getAuthority().equals(EmailContent.AUTHORITY)) { 3256 // If it's one of ours, retrieve the attachment and add it to the list 3257 long attId = Long.parseLong(attUri.getLastPathSegment()); 3258 Attachment att = Attachment.restoreAttachmentWithId(context, attId); 3259 if (att != null) { 3260 // We must clone the attachment into a new one for this message; easiest to 3261 // use a parcel here 3262 Parcel p = Parcel.obtain(); 3263 att.writeToParcel(p, 0); 3264 p.setDataPosition(0); 3265 Attachment attClone = new Attachment(p); 3266 p.recycle(); 3267 // Clear the messageKey (this is going to be a new attachment) 3268 attClone.mMessageKey = 0; 3269 // If we're sending this, it's not loaded, and we're not smart forwarding 3270 // add the download flag, so that ADS will start up 3271 if (mailbox.mType == Mailbox.TYPE_OUTBOX && att.mContentUri == null && 3272 ((account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) { 3273 attClone.mFlags |= Attachment.FLAG_DOWNLOAD_FORWARD; 3274 hasUnloadedAttachments = true; 3275 } 3276 atts.add(attClone); 3277 } 3278 } else { 3279 // Convert external attachment to one of ours and add to the list 3280 atts.add(convertUiAttachmentToAttachment(uiAtt)); 3281 } 3282 } 3283 if (!atts.isEmpty()) { 3284 msg.mAttachments = atts; 3285 msg.mFlagAttachment = true; 3286 if (hasUnloadedAttachments) { 3287 Utility.showToast(context, R.string.message_view_attachment_background_load); 3288 } 3289 } 3290 // Save it or update it... 3291 if (!msg.isSaved()) { 3292 msg.save(context); 3293 } else { 3294 // This is tricky due to how messages/attachments are saved; rather than putz with 3295 // what's changed, we'll delete/re-add them 3296 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 3297 // Delete all existing attachments 3298 ops.add(ContentProviderOperation.newDelete( 3299 ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId)) 3300 .build()); 3301 // Delete the body 3302 ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI) 3303 .withSelection(Body.MESSAGE_KEY + "=?", new String[] {Long.toString(msg.mId)}) 3304 .build()); 3305 // Add the ops for the message, atts, and body 3306 msg.addSaveOps(ops); 3307 // Do it! 3308 try { 3309 applyBatch(ops); 3310 } catch (OperationApplicationException e) { 3311 } 3312 } 3313 if (mailbox.mType == Mailbox.TYPE_OUTBOX) { 3314 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 3315 mServiceCallback, mailbox.mAccountKey); 3316 try { 3317 service.startSync(mailbox.mId, true); 3318 } catch (RemoteException e) { 3319 } 3320 long originalMsgId = msg.mSourceKey; 3321 if (originalMsgId != 0) { 3322 Message originalMsg = Message.restoreMessageWithId(context, originalMsgId); 3323 // If the original message exists, set its forwarded/replied to flags 3324 if (originalMsg != null) { 3325 ContentValues cv = new ContentValues(); 3326 flags = originalMsg.mFlags; 3327 switch(draftType) { 3328 case DraftType.FORWARD: 3329 flags |= Message.FLAG_FORWARDED; 3330 break; 3331 case DraftType.REPLY_ALL: 3332 case DraftType.REPLY: 3333 flags |= Message.FLAG_REPLIED_TO; 3334 break; 3335 } 3336 cv.put(Message.FLAGS, flags); 3337 context.getContentResolver().update(ContentUris.withAppendedId( 3338 Message.CONTENT_URI, originalMsgId), cv, null, null); 3339 } 3340 } 3341 } 3342 return uiUri("uimessage", msg.mId); 3343 } 3344 3345 /** 3346 * Create and send the message via the account indicated in the uri 3347 * @param uri the incoming uri 3348 * @param values the content values that represent message fields 3349 * @return the uri of the created message 3350 */ 3351 private Uri uiSendMail(Uri uri, ContentValues values) { 3352 List<String> pathSegments = uri.getPathSegments(); 3353 Mailbox mailbox = getMailboxByAccountIdAndType(pathSegments.get(1), Mailbox.TYPE_OUTBOX); 3354 if (mailbox == null) return null; 3355 Message msg = getMessageFromPathSegments(pathSegments); 3356 try { 3357 return uiSaveMessage(msg, mailbox, values); 3358 } finally { 3359 // Kick observers 3360 getContext().getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 3361 } 3362 } 3363 3364 /** 3365 * Create a message and save it to the drafts folder of the account indicated in the uri 3366 * @param uri the incoming uri 3367 * @param values the content values that represent message fields 3368 * @return the uri of the created message 3369 */ 3370 private Uri uiSaveDraft(Uri uri, ContentValues values) { 3371 List<String> pathSegments = uri.getPathSegments(); 3372 Mailbox mailbox = getMailboxByAccountIdAndType(pathSegments.get(1), Mailbox.TYPE_DRAFTS); 3373 if (mailbox == null) return null; 3374 Message msg = getMessageFromPathSegments(pathSegments); 3375 return uiSaveMessage(msg, mailbox, values); 3376 } 3377 3378 private int uiUpdateDraft(Uri uri, ContentValues values) { 3379 Context context = getContext(); 3380 Message msg = Message.restoreMessageWithId(context, 3381 Long.parseLong(uri.getPathSegments().get(1))); 3382 if (msg == null) return 0; 3383 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 3384 if (mailbox == null) return 0; 3385 uiSaveMessage(msg, mailbox, values); 3386 return 1; 3387 } 3388 3389 private int uiSendDraft(Uri uri, ContentValues values) { 3390 Context context = getContext(); 3391 Message msg = Message.restoreMessageWithId(context, 3392 Long.parseLong(uri.getPathSegments().get(1))); 3393 if (msg == null) return 0; 3394 long mailboxId = Mailbox.findMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_OUTBOX); 3395 if (mailboxId == Mailbox.NO_MAILBOX) return 0; 3396 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 3397 if (mailbox == null) return 0; 3398 uiSaveMessage(msg, mailbox, values); 3399 // Kick observers 3400 context.getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 3401 return 1; 3402 } 3403 3404 private void putIntegerLongOrBoolean(ContentValues values, String columnName, Object value) { 3405 if (value instanceof Integer) { 3406 Integer intValue = (Integer)value; 3407 values.put(columnName, intValue); 3408 } else if (value instanceof Boolean) { 3409 Boolean boolValue = (Boolean)value; 3410 values.put(columnName, boolValue ? 1 : 0); 3411 } else if (value instanceof Long) { 3412 Long longValue = (Long)value; 3413 values.put(columnName, longValue); 3414 } 3415 } 3416 3417 /** 3418 * Update the timestamps for the folders specified and notifies on the recent folder URI. 3419 * @param folders 3420 * @return number of folders updated 3421 */ 3422 private int updateTimestamp(final Context context, String id, Uri[] folders){ 3423 int updated = 0; 3424 final long now = System.currentTimeMillis(); 3425 final ContentResolver resolver = context.getContentResolver(); 3426 final ContentValues touchValues = new ContentValues(); 3427 for (int i=0, size=folders.length; i < size; ++i) { 3428 touchValues.put(MailboxColumns.LAST_TOUCHED_TIME, now); 3429 LogUtils.d(TAG, "updateStamp: %s updated", folders[i]); 3430 updated += resolver.update(folders[i], touchValues, null, null); 3431 } 3432 final Uri toNotify = 3433 UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build(); 3434 LogUtils.d(TAG, "updateTimestamp: Notifying on %s", toNotify); 3435 resolver.notifyChange(toNotify, null); 3436 return updated; 3437 } 3438 3439 /** 3440 * Updates the recent folders. The values to be updated are specified as ContentValues pairs 3441 * of (Folder URI, access timestamp). Returns nonzero if successful, always. 3442 * @param uri 3443 * @param values 3444 * @return nonzero value always. 3445 */ 3446 private int uiUpdateRecentFolders(Uri uri, ContentValues values) { 3447 final int numFolders = values.size(); 3448 final String id = uri.getPathSegments().get(1); 3449 final Uri[] folders = new Uri[numFolders]; 3450 final Context context = getContext(); 3451 final NotificationController controller = NotificationController.getInstance(context); 3452 int i = 0; 3453 for (final String uriString: values.keySet()) { 3454 folders[i] = Uri.parse(uriString); 3455 try { 3456 final String mailboxIdString = folders[i].getLastPathSegment(); 3457 final long mailboxId = Long.parseLong(mailboxIdString); 3458 controller.cancelNewMessageNotification(mailboxId); 3459 } catch (NumberFormatException e) { 3460 // Keep on going... 3461 } 3462 } 3463 return updateTimestamp(context, id, folders); 3464 } 3465 3466 /** 3467 * Populates the recent folders according to the design. 3468 * @param uri 3469 * @return the number of recent folders were populated. 3470 */ 3471 private int uiPopulateRecentFolders(Uri uri) { 3472 final Context context = getContext(); 3473 final String id = uri.getLastPathSegment(); 3474 final Uri[] recentFolders = defaultRecentFolders(id); 3475 final int numFolders = recentFolders.length; 3476 if (numFolders <= 0) { 3477 return 0; 3478 } 3479 final int rowsUpdated = updateTimestamp(context, id, recentFolders); 3480 LogUtils.d(TAG, "uiPopulateRecentFolders: %d folders changed", rowsUpdated); 3481 return rowsUpdated; 3482 } 3483 3484 private int uiUpdateAttachment(Uri uri, ContentValues uiValues) { 3485 Integer stateValue = uiValues.getAsInteger(UIProvider.AttachmentColumns.STATE); 3486 if (stateValue != null) { 3487 // This is a command from UIProvider 3488 long attachmentId = Long.parseLong(uri.getLastPathSegment()); 3489 Context context = getContext(); 3490 Attachment attachment = 3491 Attachment.restoreAttachmentWithId(context, attachmentId); 3492 if (attachment == null) { 3493 // Went away; ah, well... 3494 return 0; 3495 } 3496 ContentValues values = new ContentValues(); 3497 switch (stateValue.intValue()) { 3498 case UIProvider.AttachmentState.NOT_SAVED: 3499 // Set state, try to cancel request 3500 values.put(AttachmentColumns.UI_STATE, stateValue); 3501 values.put(AttachmentColumns.FLAGS, 3502 attachment.mFlags &= ~Attachment.FLAG_DOWNLOAD_USER_REQUEST); 3503 attachment.update(context, values); 3504 return 1; 3505 case UIProvider.AttachmentState.DOWNLOADING: 3506 // Set state and destination; request download 3507 values.put(AttachmentColumns.UI_STATE, stateValue); 3508 Integer destinationValue = 3509 uiValues.getAsInteger(UIProvider.AttachmentColumns.DESTINATION); 3510 values.put(AttachmentColumns.UI_DESTINATION, 3511 destinationValue == null ? 0 : destinationValue); 3512 values.put(AttachmentColumns.FLAGS, 3513 attachment.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST); 3514 attachment.update(context, values); 3515 return 1; 3516 case UIProvider.AttachmentState.SAVED: 3517 // If this is an inline attachment, notify message has changed 3518 if (!TextUtils.isEmpty(attachment.mContentId)) { 3519 notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, attachment.mMessageKey); 3520 } 3521 return 1; 3522 } 3523 } 3524 return 0; 3525 } 3526 3527 private int uiUpdateFolder(Uri uri, ContentValues uiValues) { 3528 Uri ourUri = convertToEmailProviderUri(uri, Mailbox.CONTENT_URI, true); 3529 if (ourUri == null) return 0; 3530 ContentValues ourValues = new ContentValues(); 3531 // This should only be called via update to "recent folders" 3532 for (String columnName: uiValues.keySet()) { 3533 if (columnName.equals(MailboxColumns.LAST_TOUCHED_TIME)) { 3534 ourValues.put(MailboxColumns.LAST_TOUCHED_TIME, uiValues.getAsLong(columnName)); 3535 } 3536 } 3537 return update(ourUri, ourValues, null, null); 3538 } 3539 3540 private ContentValues convertUiMessageValues(Message message, ContentValues values) { 3541 ContentValues ourValues = new ContentValues(); 3542 for (String columnName: values.keySet()) { 3543 Object val = values.get(columnName); 3544 if (columnName.equals(UIProvider.ConversationColumns.STARRED)) { 3545 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val); 3546 } else if (columnName.equals(UIProvider.ConversationColumns.READ)) { 3547 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val); 3548 } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 3549 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val); 3550 } else if (columnName.equals(UIProvider.ConversationColumns.FOLDER_LIST)) { 3551 // Convert from folder list uri to mailbox key 3552 Uri uri = Uri.parse((String)val); 3553 Long mailboxId = Long.parseLong(uri.getLastPathSegment()); 3554 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId); 3555 } else if (columnName.equals(UIProvider.ConversationColumns.RAW_FOLDERS)) { 3556 // Ignore; this is updated by the FOLDER_LIST update above. 3557 } else if (columnName.equals(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES)) { 3558 Address[] fromList = Address.unpack(message.mFrom); 3559 Preferences prefs = Preferences.getPreferences(getContext()); 3560 for (Address sender : fromList) { 3561 String email = sender.getAddress(); 3562 prefs.setSenderAsTrusted(email); 3563 } 3564 } else { 3565 throw new IllegalArgumentException("Can't update " + columnName + " in message"); 3566 } 3567 } 3568 return ourValues; 3569 } 3570 3571 private Uri convertToEmailProviderUri(Uri uri, Uri newBaseUri, boolean asProvider) { 3572 String idString = uri.getLastPathSegment(); 3573 try { 3574 long id = Long.parseLong(idString); 3575 Uri ourUri = ContentUris.withAppendedId(newBaseUri, id); 3576 if (asProvider) { 3577 ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build(); 3578 } 3579 return ourUri; 3580 } catch (NumberFormatException e) { 3581 return null; 3582 } 3583 } 3584 3585 private Message getMessageFromLastSegment(Uri uri) { 3586 long messageId = Long.parseLong(uri.getLastPathSegment()); 3587 return Message.restoreMessageWithId(getContext(), messageId); 3588 } 3589 3590 /** 3591 * Add an undo operation for the current sequence; if the sequence is newer than what we've had, 3592 * clear out the undo list and start over 3593 * @param uri the uri we're working on 3594 * @param op the ContentProviderOperation to perform upon undo 3595 */ 3596 private void addToSequence(Uri uri, ContentProviderOperation op) { 3597 String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER); 3598 if (sequenceString != null) { 3599 int sequence = Integer.parseInt(sequenceString); 3600 if (sequence > mLastSequence) { 3601 // Reset sequence 3602 mLastSequenceOps.clear(); 3603 mLastSequence = sequence; 3604 } 3605 // TODO: Need something to indicate a change isn't ready (undoable) 3606 mLastSequenceOps.add(op); 3607 } 3608 } 3609 3610 // TODO: This should depend on flags on the mailbox... 3611 private boolean uploadsToServer(Context context, Mailbox m) { 3612 if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX || 3613 m.mType == Mailbox.TYPE_SEARCH) { 3614 return false; 3615 } 3616 String protocol = Account.getProtocol(context, m.mAccountKey); 3617 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 3618 return (info != null && info.syncChanges); 3619 } 3620 3621 private int uiUpdateMessage(Uri uri, ContentValues values) { 3622 Context context = getContext(); 3623 Message msg = getMessageFromLastSegment(uri); 3624 if (msg == null) return 0; 3625 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 3626 if (mailbox == null) return 0; 3627 Uri ourBaseUri = uploadsToServer(context, mailbox) ? Message.SYNCED_CONTENT_URI : 3628 Message.CONTENT_URI; 3629 Uri ourUri = convertToEmailProviderUri(uri, ourBaseUri, true); 3630 if (ourUri == null) return 0; 3631 3632 // Special case - meeting response 3633 if (values.containsKey(UIProvider.MessageOperations.RESPOND_COLUMN)) { 3634 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 3635 mServiceCallback, mailbox.mAccountKey); 3636 try { 3637 service.sendMeetingResponse(msg.mId, 3638 values.getAsInteger(UIProvider.MessageOperations.RESPOND_COLUMN)); 3639 // Delete the message immediately 3640 uiDeleteMessage(uri); 3641 Utility.showToast(context, R.string.confirm_response); 3642 // Notify box has changed so the deletion is reflected in the UI 3643 notifyUIConversationMailbox(mailbox.mId); 3644 } catch (RemoteException e) { 3645 } 3646 return 1; 3647 } 3648 3649 ContentValues undoValues = new ContentValues(); 3650 ContentValues ourValues = convertUiMessageValues(msg, values); 3651 for (String columnName: ourValues.keySet()) { 3652 if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 3653 undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey); 3654 } else if (columnName.equals(MessageColumns.FLAG_READ)) { 3655 undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead); 3656 } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) { 3657 undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite); 3658 } 3659 } 3660 if (undoValues == null || undoValues.size() == 0) { 3661 return -1; 3662 } 3663 ContentProviderOperation op = 3664 ContentProviderOperation.newUpdate(convertToEmailProviderUri( 3665 uri, ourBaseUri, false)) 3666 .withValues(undoValues) 3667 .build(); 3668 addToSequence(uri, op); 3669 return update(ourUri, ourValues, null, null); 3670 } 3671 3672 private int uiDeleteMessage(Uri uri) { 3673 Context context = getContext(); 3674 Message msg = getMessageFromLastSegment(uri); 3675 if (msg == null) return 0; 3676 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 3677 if (mailbox == null) return 0; 3678 if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_DRAFTS) { 3679 // We actually delete these, including attachments 3680 AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId); 3681 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, mailbox.mId); 3682 return context.getContentResolver().delete( 3683 ContentUris.withAppendedId(Message.CONTENT_URI, msg.mId), null, null); 3684 } 3685 Mailbox trashMailbox = 3686 Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH); 3687 if (trashMailbox == null) return 0; 3688 ContentValues values = new ContentValues(); 3689 values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId); 3690 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, mailbox.mId); 3691 return uiUpdateMessage(uri, values); 3692 } 3693 3694 private Cursor uiUndo(String[] projection) { 3695 // First see if we have any operations saved 3696 // TODO: Make sure seq matches 3697 if (!mLastSequenceOps.isEmpty()) { 3698 try { 3699 // TODO Always use this projection? Or what's passed in? 3700 // Not sure if UI wants it, but I'm making a cursor of convo uri's 3701 MatrixCursor c = new MatrixCursor( 3702 new String[] {UIProvider.ConversationColumns.URI}, 3703 mLastSequenceOps.size()); 3704 for (ContentProviderOperation op: mLastSequenceOps) { 3705 c.addRow(new String[] {op.getUri().toString()}); 3706 } 3707 // Just apply the batch and we're done! 3708 applyBatch(mLastSequenceOps); 3709 // But clear the operations 3710 mLastSequenceOps.clear(); 3711 // Tell the UI there are changes 3712 ContentResolver resolver = getContext().getContentResolver(); 3713 resolver.notifyChange(UIPROVIDER_CONVERSATION_NOTIFIER, null); 3714 resolver.notifyChange(UIPROVIDER_FOLDER_NOTIFIER, null); 3715 return c; 3716 } catch (OperationApplicationException e) { 3717 } 3718 } 3719 return new MatrixCursor(projection, 0); 3720 } 3721 3722 private void notifyUIConversation(Uri uri) { 3723 String id = uri.getLastPathSegment(); 3724 Message msg = Message.restoreMessageWithId(getContext(), Long.parseLong(id)); 3725 if (msg != null) { 3726 notifyUIConversationMailbox(msg.mMailboxKey); 3727 } 3728 } 3729 3730 /** 3731 * Notify about the Mailbox id passed in 3732 * @param id the Mailbox id to be notified 3733 */ 3734 private void notifyUIConversationMailbox(long id) { 3735 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(id)); 3736 Mailbox mailbox = Mailbox.restoreMailboxWithId(getContext(), id); 3737 // Notify combined inbox... 3738 if (mailbox.mType == Mailbox.TYPE_INBOX) { 3739 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, 3740 EmailProvider.combinedMailboxId(Mailbox.TYPE_INBOX)); 3741 } 3742 notifyWidgets(id); 3743 } 3744 3745 private void notifyUI(Uri uri, String id) { 3746 Uri notifyUri = uri.buildUpon().appendPath(id).build(); 3747 getContext().getContentResolver().notifyChange(notifyUri, null); 3748 } 3749 3750 private void notifyUI(Uri uri, long id) { 3751 notifyUI(uri, Long.toString(id)); 3752 } 3753 3754 /** 3755 * Support for services and service notifications 3756 */ 3757 3758 private final IEmailServiceCallback.Stub mServiceCallback = 3759 new IEmailServiceCallback.Stub() { 3760 3761 @Override 3762 public void syncMailboxListStatus(long accountId, int statusCode, int progress) 3763 throws RemoteException { 3764 } 3765 3766 @Override 3767 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) 3768 throws RemoteException { 3769 // We'll get callbacks here from the services, which we'll pass back to the UI 3770 Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, mailboxId); 3771 EmailProvider.this.getContext().getContentResolver().notifyChange(uri, null); 3772 } 3773 3774 @Override 3775 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 3776 int progress) throws RemoteException { 3777 } 3778 3779 @Override 3780 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) 3781 throws RemoteException { 3782 } 3783 3784 @Override 3785 public void loadMessageStatus(long messageId, int statusCode, int progress) 3786 throws RemoteException { 3787 } 3788 }; 3789 3790 private Cursor uiFolderRefresh(Uri uri) { 3791 Context context = getContext(); 3792 String idString = uri.getLastPathSegment(); 3793 long id = Long.parseLong(idString); 3794 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, id); 3795 if (mailbox == null) return null; 3796 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 3797 mServiceCallback, mailbox.mAccountKey); 3798 try { 3799 service.startSync(id, true); 3800 } catch (RemoteException e) { 3801 } 3802 return null; 3803 } 3804 3805 //Number of additional messages to load when a user selects "Load more..." in POP/IMAP boxes 3806 public static final int VISIBLE_LIMIT_INCREMENT = 10; 3807 //Number of additional messages to load when a user selects "Load more..." in a search 3808 public static final int SEARCH_MORE_INCREMENT = 10; 3809 3810 private Cursor uiFolderLoadMore(Uri uri) { 3811 Context context = getContext(); 3812 String idString = uri.getLastPathSegment(); 3813 long id = Long.parseLong(idString); 3814 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, id); 3815 if (mailbox == null) return null; 3816 if (mailbox.mType == Mailbox.TYPE_SEARCH) { 3817 // Ask for 10 more messages 3818 mSearchParams.mOffset += SEARCH_MORE_INCREMENT; 3819 runSearchQuery(context, mailbox.mAccountKey, id); 3820 } else { 3821 ContentValues values = new ContentValues(); 3822 values.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT); 3823 values.put(EmailContent.ADD_COLUMN_NAME, VISIBLE_LIMIT_INCREMENT); 3824 Uri mailboxUri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, id); 3825 // Increase the limit 3826 context.getContentResolver().update(mailboxUri, values, null, null); 3827 // And order a refresh 3828 uiFolderRefresh(uri); 3829 } 3830 return null; 3831 } 3832 3833 private static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__"; 3834 private SearchParams mSearchParams; 3835 3836 /** 3837 * Returns the search mailbox for the specified account, creating one if necessary 3838 * @return the search mailbox for the passed in account 3839 */ 3840 private Mailbox getSearchMailbox(long accountId) { 3841 Context context = getContext(); 3842 Mailbox m = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SEARCH); 3843 if (m == null) { 3844 m = new Mailbox(); 3845 m.mAccountKey = accountId; 3846 m.mServerId = SEARCH_MAILBOX_SERVER_ID; 3847 m.mFlagVisible = false; 3848 m.mDisplayName = SEARCH_MAILBOX_SERVER_ID; 3849 m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 3850 m.mType = Mailbox.TYPE_SEARCH; 3851 m.mFlags = Mailbox.FLAG_HOLDS_MAIL; 3852 m.mParentKey = Mailbox.NO_MAILBOX; 3853 m.save(context); 3854 } 3855 return m; 3856 } 3857 3858 private void runSearchQuery(final Context context, final long accountId, 3859 final long searchMailboxId) { 3860 // Start the search running in the background 3861 new Thread(new Runnable() { 3862 @Override 3863 public void run() { 3864 try { 3865 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 3866 mServiceCallback, accountId); 3867 if (service != null) { 3868 try { 3869 // Save away the total count 3870 mSearchParams.mTotalCount = service.searchMessages(accountId, 3871 mSearchParams, searchMailboxId); 3872 //Log.d(TAG, "TotalCount to UI: " + mSearchParams.mTotalCount); 3873 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, searchMailboxId); 3874 } catch (RemoteException e) { 3875 Log.e("searchMessages", "RemoteException", e); 3876 } 3877 } 3878 } finally { 3879 } 3880 }}).start(); 3881 3882 } 3883 3884 // TODO: Handle searching for more... 3885 private Cursor uiSearch(Uri uri, String[] projection) { 3886 final long accountId = Long.parseLong(uri.getLastPathSegment()); 3887 3888 // TODO: Check the actual mailbox 3889 Mailbox inbox = Mailbox.restoreMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX); 3890 if (inbox == null) return null; 3891 3892 String filter = uri.getQueryParameter(UIProvider.SearchQueryParameters.QUERY); 3893 if (filter == null) { 3894 throw new IllegalArgumentException("No query parameter in search query"); 3895 } 3896 3897 // Find/create our search mailbox 3898 Mailbox searchMailbox = getSearchMailbox(accountId); 3899 final long searchMailboxId = searchMailbox.mId; 3900 3901 mSearchParams = new SearchParams(inbox.mId, filter, searchMailboxId); 3902 3903 final Context context = getContext(); 3904 if (mSearchParams.mOffset == 0) { 3905 // Delete existing contents of search mailbox 3906 ContentResolver resolver = context.getContentResolver(); 3907 resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId, 3908 null); 3909 ContentValues cv = new ContentValues(); 3910 // For now, use the actual query as the name of the mailbox 3911 cv.put(Mailbox.DISPLAY_NAME, mSearchParams.mFilter); 3912 resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), 3913 cv, null, null); 3914 } 3915 3916 // Start the search running in the background 3917 runSearchQuery(context, accountId, searchMailboxId); 3918 3919 // This will look just like a "normal" folder 3920 return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI, 3921 searchMailbox.mId), projection); 3922 } 3923 3924 private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; 3925 private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION = 3926 MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" + 3927 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX; 3928 private static final String MESSAGES_FOR_ACCOUNT_SELECTION = MessageColumns.ACCOUNT_KEY + "=?"; 3929 3930 /** 3931 * Delete an account and clean it up 3932 */ 3933 private int uiDeleteAccount(Uri uri) { 3934 Context context = getContext(); 3935 long accountId = Long.parseLong(uri.getLastPathSegment()); 3936 try { 3937 // Get the account URI. 3938 final Account account = Account.restoreAccountWithId(context, accountId); 3939 if (account == null) { 3940 return 0; // Already deleted? 3941 } 3942 3943 deleteAccountData(context, accountId); 3944 3945 // Now delete the account itself 3946 uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 3947 context.getContentResolver().delete(uri, null, null); 3948 3949 // Clean up 3950 AccountBackupRestore.backup(context); 3951 SecurityPolicy.getInstance(context).reducePolicies(); 3952 MailActivityEmail.setServicesEnabledSync(context); 3953 return 1; 3954 } catch (Exception e) { 3955 Log.w(Logging.LOG_TAG, "Exception while deleting account", e); 3956 } 3957 return 0; 3958 } 3959 3960 private int uiDeleteAccountData(Uri uri) { 3961 Context context = getContext(); 3962 long accountId = Long.parseLong(uri.getLastPathSegment()); 3963 // Get the account URI. 3964 final Account account = Account.restoreAccountWithId(context, accountId); 3965 if (account == null) { 3966 return 0; // Already deleted? 3967 } 3968 deleteAccountData(context, accountId); 3969 return 1; 3970 } 3971 3972 private void deleteAccountData(Context context, long accountId) { 3973 // Delete synced attachments 3974 AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId); 3975 3976 // Delete synced email, leaving only an empty inbox. We do this in two phases: 3977 // 1. Delete all non-inbox mailboxes (which will delete all of their messages) 3978 // 2. Delete all remaining messages (which will be the inbox messages) 3979 ContentResolver resolver = context.getContentResolver(); 3980 String[] accountIdArgs = new String[] { Long.toString(accountId) }; 3981 resolver.delete(Mailbox.CONTENT_URI, 3982 MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION, 3983 accountIdArgs); 3984 resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs); 3985 3986 // Delete sync keys on remaining items 3987 ContentValues cv = new ContentValues(); 3988 cv.putNull(Account.SYNC_KEY); 3989 resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs); 3990 cv.clear(); 3991 cv.putNull(Mailbox.SYNC_KEY); 3992 resolver.update(Mailbox.CONTENT_URI, cv, 3993 MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs); 3994 3995 // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable 3996 IEmailService service = EmailServiceUtils.getServiceForAccount(context, null, accountId); 3997 if (service != null) { 3998 try { 3999 service.deleteAccountPIMData(accountId); 4000 } catch (RemoteException e) { 4001 // Can't do anything about this 4002 } 4003 } 4004 } 4005 4006 private int[] mSavedWidgetIds = new int[0]; 4007 private ArrayList<Long> mWidgetNotifyMailboxes = new ArrayList<Long>(); 4008 private AppWidgetManager mAppWidgetManager; 4009 private ComponentName mEmailComponent; 4010 4011 private void notifyWidgets(long mailboxId) { 4012 Context context = getContext(); 4013 // Lazily initialize these 4014 if (mAppWidgetManager == null) { 4015 mAppWidgetManager = AppWidgetManager.getInstance(context); 4016 mEmailComponent = new ComponentName(context, WidgetProvider.PROVIDER_NAME); 4017 } 4018 4019 // See if we have to populate our array of mailboxes used in widgets 4020 int[] widgetIds = mAppWidgetManager.getAppWidgetIds(mEmailComponent); 4021 if (!Arrays.equals(widgetIds, mSavedWidgetIds)) { 4022 mSavedWidgetIds = widgetIds; 4023 String[][] widgetInfos = BaseWidgetProvider.getWidgetInfo(context, widgetIds); 4024 // widgetInfo now has pairs of account uri/folder uri 4025 mWidgetNotifyMailboxes.clear(); 4026 for (String[] widgetInfo: widgetInfos) { 4027 try { 4028 if (widgetInfo == null) continue; 4029 long id = Long.parseLong(Uri.parse(widgetInfo[1]).getLastPathSegment()); 4030 if (!isCombinedMailbox(id)) { 4031 // For a regular mailbox, just add it to the list 4032 if (!mWidgetNotifyMailboxes.contains(id)) { 4033 mWidgetNotifyMailboxes.add(id); 4034 } 4035 } else { 4036 switch (getVirtualMailboxType(id)) { 4037 // We only handle the combined inbox in widgets 4038 case Mailbox.TYPE_INBOX: 4039 Cursor c = query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION, 4040 MailboxColumns.TYPE + "=?", 4041 new String[] {Integer.toString(Mailbox.TYPE_INBOX)}, null); 4042 try { 4043 while (c.moveToNext()) { 4044 mWidgetNotifyMailboxes.add( 4045 c.getLong(Mailbox.ID_PROJECTION_COLUMN)); 4046 } 4047 } finally { 4048 c.close(); 4049 } 4050 break; 4051 } 4052 } 4053 } catch (NumberFormatException e) { 4054 // Move along 4055 } 4056 } 4057 } 4058 4059 // If our mailbox needs to be notified, do so... 4060 if (mWidgetNotifyMailboxes.contains(mailboxId)) { 4061 Intent intent = new Intent(Utils.ACTION_NOTIFY_DATASET_CHANGED); 4062 intent.putExtra(Utils.EXTRA_FOLDER_URI, uiUri("uifolder", mailboxId)); 4063 intent.setType(EMAIL_APP_MIME_TYPE); 4064 context.sendBroadcast(intent); 4065 } 4066 } 4067} 4068