EmailProvider.java revision 0203a06c87e389f0d3af19bc26f61d0cf846226a
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.PeriodicSync; 31import android.content.UriMatcher; 32import android.content.res.Resources; 33import android.database.ContentObserver; 34import android.database.Cursor; 35import android.database.CursorWrapper; 36import android.database.DatabaseUtils; 37import android.database.MatrixCursor; 38import android.database.MergeCursor; 39import android.database.sqlite.SQLiteDatabase; 40import android.database.sqlite.SQLiteException; 41import android.net.Uri; 42import android.os.AsyncTask; 43import android.os.Binder; 44import android.os.Bundle; 45import android.os.Parcel; 46import android.os.ParcelFileDescriptor; 47import android.os.RemoteException; 48import android.provider.BaseColumns; 49import android.text.TextUtils; 50import android.text.format.DateUtils; 51import android.util.Base64; 52import android.util.SparseArray; 53 54import com.android.common.content.ProjectionMap; 55import com.android.email.Preferences; 56import com.android.email.R; 57import com.android.email.SecurityPolicy; 58import com.android.email.service.AttachmentDownloadService; 59import com.android.email.service.EmailServiceUtils; 60import com.android.email.service.EmailServiceUtils.EmailServiceInfo; 61import com.android.email2.ui.MailActivityEmail; 62import com.android.emailcommon.Logging; 63import com.android.emailcommon.mail.Address; 64import com.android.emailcommon.provider.Account; 65import com.android.emailcommon.provider.EmailContent; 66import com.android.emailcommon.provider.EmailContent.AccountColumns; 67import com.android.emailcommon.provider.EmailContent.Attachment; 68import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 69import com.android.emailcommon.provider.EmailContent.Body; 70import com.android.emailcommon.provider.EmailContent.BodyColumns; 71import com.android.emailcommon.provider.EmailContent.MailboxColumns; 72import com.android.emailcommon.provider.EmailContent.Message; 73import com.android.emailcommon.provider.EmailContent.MessageColumns; 74import com.android.emailcommon.provider.EmailContent.PolicyColumns; 75import com.android.emailcommon.provider.EmailContent.SyncColumns; 76import com.android.emailcommon.provider.HostAuth; 77import com.android.emailcommon.provider.Mailbox; 78import com.android.emailcommon.provider.MessageChangeLogTable; 79import com.android.emailcommon.provider.MessageMove; 80import com.android.emailcommon.provider.MessageStateChange; 81import com.android.emailcommon.provider.Policy; 82import com.android.emailcommon.provider.QuickResponse; 83import com.android.emailcommon.service.EmailServiceProxy; 84import com.android.emailcommon.service.EmailServiceStatus; 85import com.android.emailcommon.service.IEmailService; 86import com.android.emailcommon.service.SearchParams; 87import com.android.emailcommon.utility.AttachmentUtilities; 88import com.android.emailcommon.utility.Utility; 89import com.android.ex.photo.provider.PhotoContract; 90import com.android.mail.preferences.MailPrefs; 91import com.android.mail.providers.ConversationInfo; 92import com.android.mail.providers.Folder; 93import com.android.mail.providers.FolderList; 94import com.android.mail.providers.MessageInfo; 95import com.android.mail.providers.UIProvider; 96import com.android.mail.providers.UIProvider.AccountCapabilities; 97import com.android.mail.providers.UIProvider.AccountCursorExtraKeys; 98import com.android.mail.providers.UIProvider.ConversationColumns; 99import com.android.mail.providers.UIProvider.ConversationPriority; 100import com.android.mail.providers.UIProvider.ConversationSendingState; 101import com.android.mail.providers.UIProvider.DraftType; 102import com.android.mail.utils.AttachmentUtils; 103import com.android.mail.utils.LogTag; 104import com.android.mail.utils.LogUtils; 105import com.android.mail.utils.MatrixCursorWithCachedColumns; 106import com.android.mail.utils.MatrixCursorWithExtra; 107import com.android.mail.utils.MimeType; 108import com.android.mail.utils.Utils; 109import com.android.mail.widget.BaseWidgetProvider; 110import com.android.mail.widget.WidgetProvider; 111import com.google.common.collect.ImmutableMap; 112import com.google.common.collect.ImmutableSet; 113import com.google.common.collect.Lists; 114 115import java.io.File; 116import java.io.FileDescriptor; 117import java.io.FileNotFoundException; 118import java.io.PrintWriter; 119import java.util.ArrayList; 120import java.util.Arrays; 121import java.util.List; 122import java.util.Locale; 123import java.util.Map; 124import java.util.Set; 125import java.util.regex.Pattern; 126 127/** 128 * @author mblank 129 * 130 */ 131public class EmailProvider extends ContentProvider { 132 133 private static final String TAG = LogTag.getLogTag(); 134 135 public static String EMAIL_APP_MIME_TYPE; 136 137 private static final String DATABASE_NAME = "EmailProvider.db"; 138 private static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; 139 private static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db"; 140 141 /** 142 * Notifies that changes happened. Certain UI components, e.g., widgets, can register for this 143 * {@link android.content.Intent} and update accordingly. However, this can be very broad and 144 * is NOT the preferred way of getting notification. 145 */ 146 private static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED = 147 "com.android.email.MESSAGE_LIST_DATASET_CHANGED"; 148 149 private static final String EMAIL_MESSAGE_MIME_TYPE = 150 "vnd.android.cursor.item/email-message"; 151 private static final String EMAIL_ATTACHMENT_MIME_TYPE = 152 "vnd.android.cursor.item/email-attachment"; 153 154 /** Appended to the notification URI for delete operations */ 155 private static final String NOTIFICATION_OP_DELETE = "delete"; 156 /** Appended to the notification URI for insert operations */ 157 private static final String NOTIFICATION_OP_INSERT = "insert"; 158 /** Appended to the notification URI for update operations */ 159 private static final String NOTIFICATION_OP_UPDATE = "update"; 160 161 /** The query string to trigger a folder refresh. */ 162 private static String QUERY_UIREFRESH = "uirefresh"; 163 164 // Definitions for our queries looking for orphaned messages 165 private static final String[] ORPHANS_PROJECTION 166 = new String[] {MessageColumns.ID, MessageColumns.MAILBOX_KEY}; 167 private static final int ORPHANS_ID = 0; 168 private static final int ORPHANS_MAILBOX_KEY = 1; 169 170 private static final String WHERE_ID = EmailContent.RECORD_ID + "=?"; 171 172 private static final int ACCOUNT_BASE = 0; 173 private static final int ACCOUNT = ACCOUNT_BASE; 174 private static final int ACCOUNT_ID = ACCOUNT_BASE + 1; 175 private static final int ACCOUNT_RESET_NEW_COUNT = ACCOUNT_BASE + 2; 176 private static final int ACCOUNT_RESET_NEW_COUNT_ID = ACCOUNT_BASE + 3; 177 private static final int ACCOUNT_CHECK = ACCOUNT_BASE + 4; 178 private static final int ACCOUNT_PICK_TRASH_FOLDER = ACCOUNT_BASE + 5; 179 private static final int ACCOUNT_PICK_SENT_FOLDER = ACCOUNT_BASE + 6; 180 181 private static final int MAILBOX_BASE = 0x1000; 182 private static final int MAILBOX = MAILBOX_BASE; 183 private static final int MAILBOX_ID = MAILBOX_BASE + 1; 184 private static final int MAILBOX_NOTIFICATION = MAILBOX_BASE + 2; 185 private static final int MAILBOX_MOST_RECENT_MESSAGE = MAILBOX_BASE + 3; 186 private static final int MAILBOX_MESSAGE_COUNT = MAILBOX_BASE + 4; 187 188 private static final int MESSAGE_BASE = 0x2000; 189 private static final int MESSAGE = MESSAGE_BASE; 190 private static final int MESSAGE_ID = MESSAGE_BASE + 1; 191 private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2; 192 private static final int MESSAGE_SELECTION = MESSAGE_BASE + 3; 193 private static final int MESSAGE_MOVE = MESSAGE_BASE + 4; 194 private static final int MESSAGE_STATE_CHANGE = MESSAGE_BASE + 5; 195 196 private static final int ATTACHMENT_BASE = 0x3000; 197 private static final int ATTACHMENT = ATTACHMENT_BASE; 198 private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1; 199 private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2; 200 private static final int ATTACHMENTS_CACHED_FILE_ACCESS = ATTACHMENT_BASE + 3; 201 202 private static final int HOSTAUTH_BASE = 0x4000; 203 private static final int HOSTAUTH = HOSTAUTH_BASE; 204 private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1; 205 206 private static final int UPDATED_MESSAGE_BASE = 0x5000; 207 private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE; 208 private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1; 209 210 private static final int DELETED_MESSAGE_BASE = 0x6000; 211 private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE; 212 private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1; 213 214 private static final int POLICY_BASE = 0x7000; 215 private static final int POLICY = POLICY_BASE; 216 private static final int POLICY_ID = POLICY_BASE + 1; 217 218 private static final int QUICK_RESPONSE_BASE = 0x8000; 219 private static final int QUICK_RESPONSE = QUICK_RESPONSE_BASE; 220 private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1; 221 private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2; 222 223 private static final int UI_BASE = 0x9000; 224 private static final int UI_FOLDERS = UI_BASE; 225 private static final int UI_SUBFOLDERS = UI_BASE + 1; 226 private static final int UI_MESSAGES = UI_BASE + 2; 227 private static final int UI_MESSAGE = UI_BASE + 3; 228 private static final int UI_UNDO = UI_BASE + 4; 229 private static final int UI_FOLDER_REFRESH = UI_BASE + 5; 230 private static final int UI_FOLDER = UI_BASE + 6; 231 private static final int UI_ACCOUNT = UI_BASE + 7; 232 private static final int UI_ACCTS = UI_BASE + 8; 233 private static final int UI_ATTACHMENTS = UI_BASE + 9; 234 private static final int UI_ATTACHMENT = UI_BASE + 10; 235 private static final int UI_SEARCH = UI_BASE + 11; 236 private static final int UI_ACCOUNT_DATA = UI_BASE + 12; 237 private static final int UI_FOLDER_LOAD_MORE = UI_BASE + 13; 238 private static final int UI_CONVERSATION = UI_BASE + 14; 239 private static final int UI_RECENT_FOLDERS = UI_BASE + 15; 240 private static final int UI_DEFAULT_RECENT_FOLDERS = UI_BASE + 16; 241 private static final int UI_ALL_FOLDERS = UI_BASE + 17; 242 243 private static final int BODY_BASE = 0xA000; 244 private static final int BODY = BODY_BASE; 245 private static final int BODY_ID = BODY_BASE + 1; 246 247 private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc. 248 249 private static final SparseArray<String> TABLE_NAMES; 250 static { 251 SparseArray<String> array = new SparseArray<String>(11); 252 array.put(ACCOUNT_BASE >> BASE_SHIFT, Account.TABLE_NAME); 253 array.put(MAILBOX_BASE >> BASE_SHIFT, Mailbox.TABLE_NAME); 254 array.put(MESSAGE_BASE >> BASE_SHIFT, Message.TABLE_NAME); 255 array.put(ATTACHMENT_BASE >> BASE_SHIFT, Attachment.TABLE_NAME); 256 array.put(HOSTAUTH_BASE >> BASE_SHIFT, HostAuth.TABLE_NAME); 257 array.put(UPDATED_MESSAGE_BASE >> BASE_SHIFT, Message.UPDATED_TABLE_NAME); 258 array.put(DELETED_MESSAGE_BASE >> BASE_SHIFT, Message.DELETED_TABLE_NAME); 259 array.put(POLICY_BASE >> BASE_SHIFT, Policy.TABLE_NAME); 260 array.put(QUICK_RESPONSE_BASE >> BASE_SHIFT, QuickResponse.TABLE_NAME); 261 array.put(UI_BASE >> BASE_SHIFT, null); 262 array.put(BODY_BASE >> BASE_SHIFT, Body.TABLE_NAME); 263 TABLE_NAMES = array; 264 } 265 266 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 267 268 /** 269 * Functions which manipulate the database connection or files synchronize on this. 270 * It's static because there can be multiple provider objects. 271 * TODO: Do we actually need to synchronize across all DB access, not just connection creation? 272 */ 273 private static final Object sDatabaseLock = new Object(); 274 275 /** 276 * Let's only generate these SQL strings once, as they are used frequently 277 * Note that this isn't relevant for table creation strings, since they are used only once 278 */ 279 private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " + 280 Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 281 EmailContent.RECORD_ID + '='; 282 283 private static final String UPDATED_MESSAGE_DELETE = "delete from " + 284 Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '='; 285 286 private static final String DELETED_MESSAGE_INSERT = "insert or replace into " + 287 Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 288 EmailContent.RECORD_ID + '='; 289 290 private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME + 291 " where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY + 292 " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " + 293 Message.TABLE_NAME + ')'; 294 295 private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME + 296 " where " + BodyColumns.MESSAGE_KEY + '='; 297 298 private static ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 299 private static final ContentValues EMPTY_CONTENT_VALUES = new ContentValues(); 300 301 private static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; 302 303 // For undo handling 304 private int mLastSequence = -1; 305 private final ArrayList<ContentProviderOperation> mLastSequenceOps = 306 new ArrayList<ContentProviderOperation>(); 307 308 // Query parameter indicating the command came from UIProvider 309 private static final String IS_UIPROVIDER = "is_uiprovider"; 310 311 private static final String SYNC_STATUS_CALLBACK_METHOD = "sync_status"; 312 313 /** 314 * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in 315 * @param uri the Uri to match 316 * @return the match value 317 */ 318 private static int findMatch(Uri uri, String methodName) { 319 int match = sURIMatcher.match(uri); 320 if (match < 0) { 321 throw new IllegalArgumentException("Unknown uri: " + uri); 322 } else if (Logging.LOGD) { 323 LogUtils.v(TAG, methodName + ": uri=" + uri + ", match is " + match); 324 } 325 return match; 326 } 327 328 private static Uri INTEGRITY_CHECK_URI; 329 public static Uri ACCOUNT_BACKUP_URI; 330 private static Uri FOLDER_STATUS_URI; 331 332 private SQLiteDatabase mDatabase; 333 private SQLiteDatabase mBodyDatabase; 334 335 public static Uri uiUri(String type, long id) { 336 return Uri.parse(uiUriString(type, id)); 337 } 338 339 /** 340 * Creates a URI string from a database ID (guaranteed to be unique). 341 * @param type of the resource: uifolder, message, etc. 342 * @param id the id of the resource. 343 * @return uri string 344 */ 345 public static String uiUriString(String type, long id) { 346 return "content://" + EmailContent.AUTHORITY + "/" + type + ((id == -1) ? "" : ("/" + id)); 347 } 348 349 /** 350 * Orphan record deletion utility. Generates a sqlite statement like: 351 * delete from <table> where <column> not in (select <foreignColumn> from <foreignTable>) 352 * @param db the EmailProvider database 353 * @param table the table whose orphans are to be removed 354 * @param column the column deletion will be based on 355 * @param foreignColumn the column in the foreign table whose absence will trigger the deletion 356 * @param foreignTable the foreign table 357 */ 358 private static void deleteUnlinked(SQLiteDatabase db, String table, String column, 359 String foreignColumn, String foreignTable) { 360 int count = db.delete(table, column + " not in (select " + foreignColumn + " from " + 361 foreignTable + ")", null); 362 if (count > 0) { 363 LogUtils.w(TAG, "Found " + count + " orphaned row(s) in " + table); 364 } 365 } 366 367 private SQLiteDatabase getDatabase(Context context) { 368 synchronized (sDatabaseLock) { 369 // Always return the cached database, if we've got one 370 if (mDatabase != null) { 371 return mDatabase; 372 } 373 374 // Whenever we create or re-cache the databases, make sure that we haven't lost one 375 // to corruption 376 checkDatabases(); 377 378 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); 379 mDatabase = helper.getWritableDatabase(); 380 DBHelper.BodyDatabaseHelper bodyHelper = 381 new DBHelper.BodyDatabaseHelper(context, BODY_DATABASE_NAME); 382 mBodyDatabase = bodyHelper.getWritableDatabase(); 383 if (mBodyDatabase != null) { 384 String bodyFileName = mBodyDatabase.getPath(); 385 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); 386 } 387 388 // Restore accounts if the database is corrupted... 389 restoreIfNeeded(context, mDatabase); 390 // Check for any orphaned Messages in the updated/deleted tables 391 deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME); 392 deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME); 393 // Delete orphaned mailboxes/messages/policies (account no longer exists) 394 deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY, 395 AccountColumns.ID, Account.TABLE_NAME); 396 deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY, 397 AccountColumns.ID, Account.TABLE_NAME); 398 deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns.ID, 399 AccountColumns.POLICY_KEY, Account.TABLE_NAME); 400 initUiProvider(); 401 return mDatabase; 402 } 403 } 404 405 /** 406 * Perform startup actions related to UI 407 */ 408 private void initUiProvider() { 409 // Clear mailbox sync status 410 mDatabase.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UI_SYNC_STATUS + 411 "=" + UIProvider.SyncStatus.NO_SYNC); 412 } 413 414 /** 415 * Restore user Account and HostAuth data from our backup database 416 */ 417 private static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) { 418 if (MailActivityEmail.DEBUG) { 419 LogUtils.w(TAG, "restoreIfNeeded..."); 420 } 421 // Check for legacy backup 422 String legacyBackup = Preferences.getLegacyBackupPreference(context); 423 // If there's a legacy backup, create a new-style backup and delete the legacy backup 424 // In the 1:1000000000 chance that the user gets an app update just as his database becomes 425 // corrupt, oh well... 426 if (!TextUtils.isEmpty(legacyBackup)) { 427 backupAccounts(context, mainDatabase); 428 Preferences.clearLegacyBackupPreference(context); 429 LogUtils.w(TAG, "Created new EmailProvider backup database"); 430 return; 431 } 432 433 // If we have accounts, we're done 434 if (DatabaseUtils.longForQuery(mainDatabase, 435 "SELECT EXISTS (SELECT ? FROM " + Account.TABLE_NAME + " )", 436 EmailContent.ID_PROJECTION) > 0) { 437 if (MailActivityEmail.DEBUG) { 438 LogUtils.w(TAG, "restoreIfNeeded: Account exists."); 439 } 440 return; 441 } 442 443 restoreAccounts(context, mainDatabase); 444 } 445 446 /** {@inheritDoc} */ 447 @Override 448 public void shutdown() { 449 if (mDatabase != null) { 450 mDatabase.close(); 451 mDatabase = null; 452 } 453 if (mBodyDatabase != null) { 454 mBodyDatabase.close(); 455 mBodyDatabase = null; 456 } 457 } 458 459 private static void deleteMessageOrphans(SQLiteDatabase database, String tableName) { 460 if (database != null) { 461 // We'll look at all of the items in the table; there won't be many typically 462 Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); 463 // Usually, there will be nothing in these tables, so make a quick check 464 try { 465 if (c.getCount() == 0) return; 466 ArrayList<Long> foundMailboxes = new ArrayList<Long>(); 467 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>(); 468 ArrayList<Long> deleteList = new ArrayList<Long>(); 469 String[] bindArray = new String[1]; 470 while (c.moveToNext()) { 471 // Get the mailbox key and see if we've already found this mailbox 472 // If so, we're fine 473 long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY); 474 // If we already know this mailbox doesn't exist, mark the message for deletion 475 if (notFoundMailboxes.contains(mailboxId)) { 476 deleteList.add(c.getLong(ORPHANS_ID)); 477 // If we don't know about this mailbox, we'll try to find it 478 } else if (!foundMailboxes.contains(mailboxId)) { 479 bindArray[0] = Long.toString(mailboxId); 480 Cursor boxCursor = database.query(Mailbox.TABLE_NAME, 481 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null); 482 try { 483 // If it exists, we'll add it to the "found" mailboxes 484 if (boxCursor.moveToFirst()) { 485 foundMailboxes.add(mailboxId); 486 // Otherwise, we'll add to "not found" and mark the message for deletion 487 } else { 488 notFoundMailboxes.add(mailboxId); 489 deleteList.add(c.getLong(ORPHANS_ID)); 490 } 491 } finally { 492 boxCursor.close(); 493 } 494 } 495 } 496 // Now, delete the orphan messages 497 for (long messageId: deleteList) { 498 bindArray[0] = Long.toString(messageId); 499 database.delete(tableName, WHERE_ID, bindArray); 500 } 501 } finally { 502 c.close(); 503 } 504 } 505 } 506 507 @Override 508 public int delete(Uri uri, String selection, String[] selectionArgs) { 509 final int match = findMatch(uri, "delete"); 510 Context context = getContext(); 511 // Pick the correct database for this operation 512 // If we're in a transaction already (which would happen during applyBatch), then the 513 // body database is already attached to the email database and any attempt to use the 514 // body database directly will result in a SQLiteException (the database is locked) 515 SQLiteDatabase db = getDatabase(context); 516 int table = match >> BASE_SHIFT; 517 String id = "0"; 518 boolean messageDeletion = false; 519 ContentResolver resolver = context.getContentResolver(); 520 521 String tableName = TABLE_NAMES.valueAt(table); 522 int result = -1; 523 524 try { 525 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 526 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 527 notifyUIConversation(uri); 528 } 529 } 530 switch (match) { 531 case UI_MESSAGE: 532 return uiDeleteMessage(uri); 533 case UI_ACCOUNT_DATA: 534 return uiDeleteAccountData(uri); 535 case UI_ACCOUNT: 536 return uiDeleteAccount(uri); 537 case MESSAGE_SELECTION: 538 Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection, 539 selectionArgs, null, null, null); 540 try { 541 if (findCursor.moveToFirst()) { 542 return delete(ContentUris.withAppendedId( 543 Message.CONTENT_URI, 544 findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)), 545 null, null); 546 } else { 547 return 0; 548 } 549 } finally { 550 findCursor.close(); 551 } 552 // These are cases in which one or more Messages might get deleted, either by 553 // cascade or explicitly 554 case MAILBOX_ID: 555 case MAILBOX: 556 case ACCOUNT_ID: 557 case ACCOUNT: 558 case MESSAGE: 559 case SYNCED_MESSAGE_ID: 560 case MESSAGE_ID: 561 // Handle lost Body records here, since this cannot be done in a trigger 562 // The process is: 563 // 1) Begin a transaction, ensuring that both databases are affected atomically 564 // 2) Do the requested deletion, with cascading deletions handled in triggers 565 // 3) End the transaction, committing all changes atomically 566 // 567 // Bodies are auto-deleted here; Attachments are auto-deleted via trigger 568 messageDeletion = true; 569 db.beginTransaction(); 570 break; 571 } 572 switch (match) { 573 case BODY_ID: 574 case DELETED_MESSAGE_ID: 575 case SYNCED_MESSAGE_ID: 576 case MESSAGE_ID: 577 case UPDATED_MESSAGE_ID: 578 case ATTACHMENT_ID: 579 case MAILBOX_ID: 580 case ACCOUNT_ID: 581 case HOSTAUTH_ID: 582 case POLICY_ID: 583 case QUICK_RESPONSE_ID: 584 id = uri.getPathSegments().get(1); 585 if (match == SYNCED_MESSAGE_ID) { 586 // For synced messages, first copy the old message to the deleted table and 587 // delete it from the updated table (in case it was updated first) 588 // Note that this is all within a transaction, for atomicity 589 db.execSQL(DELETED_MESSAGE_INSERT + id); 590 db.execSQL(UPDATED_MESSAGE_DELETE + id); 591 } 592 593 final long accountId; 594 if (match == MAILBOX_ID) { 595 accountId = Mailbox.getAccountIdForMailbox(context, id); 596 } else { 597 accountId = Account.NO_ACCOUNT; 598 } 599 600 result = db.delete(tableName, whereWithId(id, selection), selectionArgs); 601 602 if (match == ACCOUNT_ID) { 603 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id); 604 resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 605 } else if (match == MAILBOX_ID) { 606 notifyUIFolder(id, accountId); 607 } else if (match == ATTACHMENT_ID) { 608 notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id); 609 } 610 break; 611 case ATTACHMENTS_MESSAGE_ID: 612 // All attachments for the given message 613 id = uri.getPathSegments().get(2); 614 result = db.delete(tableName, 615 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs); 616 break; 617 618 case BODY: 619 case MESSAGE: 620 case DELETED_MESSAGE: 621 case UPDATED_MESSAGE: 622 case ATTACHMENT: 623 case MAILBOX: 624 case ACCOUNT: 625 case HOSTAUTH: 626 case POLICY: 627 result = db.delete(tableName, selection, selectionArgs); 628 break; 629 case MESSAGE_MOVE: 630 db.delete(MessageMove.TABLE_NAME, selection, selectionArgs); 631 break; 632 case MESSAGE_STATE_CHANGE: 633 db.delete(MessageStateChange.TABLE_NAME, selection, selectionArgs); 634 break; 635 default: 636 throw new IllegalArgumentException("Unknown URI " + uri); 637 } 638 if (messageDeletion) { 639 if (match == MESSAGE_ID) { 640 // Delete the Body record associated with the deleted message 641 db.execSQL(DELETE_BODY + id); 642 } else { 643 // Delete any orphaned Body records 644 db.execSQL(DELETE_ORPHAN_BODIES); 645 } 646 db.setTransactionSuccessful(); 647 } 648 } catch (SQLiteException e) { 649 checkDatabases(); 650 throw e; 651 } finally { 652 if (messageDeletion) { 653 db.endTransaction(); 654 } 655 } 656 657 // Notify all notifier cursors 658 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id); 659 660 // Notify all email content cursors 661 resolver.notifyChange(EmailContent.CONTENT_URI, null); 662 return result; 663 } 664 665 @Override 666 // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) 667 public String getType(Uri uri) { 668 int match = findMatch(uri, "getType"); 669 switch (match) { 670 case BODY_ID: 671 return "vnd.android.cursor.item/email-body"; 672 case BODY: 673 return "vnd.android.cursor.dir/email-body"; 674 case UPDATED_MESSAGE_ID: 675 case MESSAGE_ID: 676 // NOTE: According to the framework folks, we're supposed to invent mime types as 677 // a way of passing information to drag & drop recipients. 678 // If there's a mailboxId parameter in the url, we respond with a mime type that 679 // has -n appended, where n is the mailboxId of the message. The drag & drop code 680 // uses this information to know not to allow dragging the item to its own mailbox 681 String mimeType = EMAIL_MESSAGE_MIME_TYPE; 682 String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID); 683 if (mailboxId != null) { 684 mimeType += "-" + mailboxId; 685 } 686 return mimeType; 687 case UPDATED_MESSAGE: 688 case MESSAGE: 689 return "vnd.android.cursor.dir/email-message"; 690 case MAILBOX: 691 return "vnd.android.cursor.dir/email-mailbox"; 692 case MAILBOX_ID: 693 return "vnd.android.cursor.item/email-mailbox"; 694 case ACCOUNT: 695 return "vnd.android.cursor.dir/email-account"; 696 case ACCOUNT_ID: 697 return "vnd.android.cursor.item/email-account"; 698 case ATTACHMENTS_MESSAGE_ID: 699 case ATTACHMENT: 700 return "vnd.android.cursor.dir/email-attachment"; 701 case ATTACHMENT_ID: 702 return EMAIL_ATTACHMENT_MIME_TYPE; 703 case HOSTAUTH: 704 return "vnd.android.cursor.dir/email-hostauth"; 705 case HOSTAUTH_ID: 706 return "vnd.android.cursor.item/email-hostauth"; 707 default: 708 return null; 709 } 710 } 711 712 // These URIs are used for specific UI notifications. We don't use EmailContent.CONTENT_URI 713 // as the base because that gets spammed. 714 // These can't be statically initialized because they depend on EmailContent.AUTHORITY 715 private static Uri UIPROVIDER_CONVERSATION_NOTIFIER; 716 private static Uri UIPROVIDER_FOLDER_NOTIFIER; 717 private static Uri UIPROVIDER_FOLDERLIST_NOTIFIER; 718 private static Uri UIPROVIDER_ACCOUNT_NOTIFIER; 719 // Not currently used 720 //public static Uri UIPROVIDER_SETTINGS_NOTIFIER; 721 private static Uri UIPROVIDER_ATTACHMENT_NOTIFIER; 722 private static Uri UIPROVIDER_ATTACHMENTS_NOTIFIER; 723 public static Uri UIPROVIDER_ALL_ACCOUNTS_NOTIFIER; 724 private static Uri UIPROVIDER_MESSAGE_NOTIFIER; 725 private static Uri UIPROVIDER_RECENT_FOLDERS_NOTIFIER; 726 727 @Override 728 public Uri insert(Uri uri, ContentValues values) { 729 int match = findMatch(uri, "insert"); 730 Context context = getContext(); 731 ContentResolver resolver = context.getContentResolver(); 732 733 // See the comment at delete(), above 734 SQLiteDatabase db = getDatabase(context); 735 int table = match >> BASE_SHIFT; 736 String id = "0"; 737 long longId; 738 739 // We do NOT allow setting of unreadCount/messageCount via the provider 740 // These columns are maintained via triggers 741 if (match == MAILBOX_ID || match == MAILBOX) { 742 values.put(MailboxColumns.UNREAD_COUNT, 0); 743 values.put(MailboxColumns.MESSAGE_COUNT, 0); 744 } 745 746 final Uri resultUri; 747 748 try { 749 switch (match) { 750 // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE 751 // or DELETED_MESSAGE; see the comment below for details 752 case UPDATED_MESSAGE: 753 case DELETED_MESSAGE: 754 case MESSAGE: 755 case BODY: 756 case ATTACHMENT: 757 case MAILBOX: 758 case ACCOUNT: 759 case HOSTAUTH: 760 case POLICY: 761 case QUICK_RESPONSE: 762 longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values); 763 resultUri = ContentUris.withAppendedId(uri, longId); 764 switch(match) { 765 case MESSAGE: 766 final long mailboxId = values.getAsLong(Message.MAILBOX_KEY); 767 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 768 notifyUIConversationMailbox(mailboxId); 769 } 770 notifyUIFolder(mailboxId, values.getAsLong(Message.ACCOUNT_KEY)); 771 break; 772 case MAILBOX: 773 if (values.containsKey(MailboxColumns.TYPE)) { 774 if (values.getAsInteger(MailboxColumns.TYPE) < 775 Mailbox.TYPE_NOT_EMAIL) { 776 // Notify the account when a new mailbox is added 777 final Long accountId = 778 values.getAsLong(MailboxColumns.ACCOUNT_KEY); 779 if (accountId != null && accountId > 0) { 780 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, accountId); 781 notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId); 782 } 783 } 784 } 785 break; 786 case ACCOUNT: 787 updateAccountSyncInterval(longId, values); 788 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 789 notifyUIAccount(longId); 790 } 791 resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 792 break; 793 case UPDATED_MESSAGE: 794 case DELETED_MESSAGE: 795 throw new IllegalArgumentException("Unknown URL " + uri); 796 case ATTACHMENT: 797 int flags = 0; 798 if (values.containsKey(Attachment.FLAGS)) { 799 flags = values.getAsInteger(Attachment.FLAGS); 800 } 801 // Report all new attachments to the download service 802 mAttachmentService.attachmentChanged(getContext(), longId, flags); 803 break; 804 } 805 break; 806 case QUICK_RESPONSE_ACCOUNT_ID: 807 longId = Long.parseLong(uri.getPathSegments().get(2)); 808 values.put(EmailContent.QuickResponseColumns.ACCOUNT_KEY, longId); 809 return insert(QuickResponse.CONTENT_URI, values); 810 case MAILBOX_ID: 811 // This implies adding a message to a mailbox 812 // Hmm, a problem here is that we can't link the account as well, so it must be 813 // already in the values... 814 longId = Long.parseLong(uri.getPathSegments().get(1)); 815 values.put(MessageColumns.MAILBOX_KEY, longId); 816 return insert(Message.CONTENT_URI, values); // Recurse 817 case MESSAGE_ID: 818 // This implies adding an attachment to a message. 819 id = uri.getPathSegments().get(1); 820 longId = Long.parseLong(id); 821 values.put(AttachmentColumns.MESSAGE_KEY, longId); 822 return insert(Attachment.CONTENT_URI, values); // Recurse 823 case ACCOUNT_ID: 824 // This implies adding a mailbox to an account. 825 longId = Long.parseLong(uri.getPathSegments().get(1)); 826 values.put(MailboxColumns.ACCOUNT_KEY, longId); 827 return insert(Mailbox.CONTENT_URI, values); // Recurse 828 case ATTACHMENTS_MESSAGE_ID: 829 longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values); 830 resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId); 831 break; 832 default: 833 throw new IllegalArgumentException("Unknown URL " + uri); 834 } 835 } catch (SQLiteException e) { 836 checkDatabases(); 837 throw e; 838 } 839 840 // Notify all notifier cursors 841 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id); 842 843 // Notify all existing cursors. 844 resolver.notifyChange(EmailContent.CONTENT_URI, null); 845 return resultUri; 846 } 847 848 @Override 849 public boolean onCreate() { 850 Context context = getContext(); 851 EmailContent.init(context); 852 init(context); 853 // Do this last, so that EmailContent/EmailProvider are initialized 854 MailActivityEmail.setServicesEnabledAsync(context); 855 return false; 856 } 857 858 private static void init(final Context context) { 859 // Synchronize on the matcher rather than the class object to minimize risk of contention 860 // & deadlock. 861 synchronized (sURIMatcher) { 862 // We use the existence of this variable as indicative of whether this function has 863 // already run. 864 if (INTEGRITY_CHECK_URI != null) { 865 return; 866 } 867 INTEGRITY_CHECK_URI = Uri.parse("content://" + EmailContent.AUTHORITY + 868 "/integrityCheck"); 869 ACCOUNT_BACKUP_URI = 870 Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup"); 871 FOLDER_STATUS_URI = 872 Uri.parse("content://" + EmailContent.AUTHORITY + "/status"); 873 EMAIL_APP_MIME_TYPE = context.getString(R.string.application_mime_type); 874 875 final String uiNotificationAuthority = 876 EmailContent.EMAIL_PACKAGE_NAME + ".uinotifications"; 877 UIPROVIDER_CONVERSATION_NOTIFIER = 878 Uri.parse("content://" + uiNotificationAuthority + "/uimessages"); 879 UIPROVIDER_FOLDER_NOTIFIER = 880 Uri.parse("content://" + uiNotificationAuthority + "/uifolder"); 881 UIPROVIDER_FOLDERLIST_NOTIFIER = 882 Uri.parse("content://" + uiNotificationAuthority + "/uifolders"); 883 UIPROVIDER_ACCOUNT_NOTIFIER = 884 Uri.parse("content://" + uiNotificationAuthority + "/uiaccount"); 885 // Not currently used 886 /* UIPROVIDER_SETTINGS_NOTIFIER = 887 Uri.parse("content://" + uiNotificationAuthority + "/uisettings");*/ 888 UIPROVIDER_ATTACHMENT_NOTIFIER = 889 Uri.parse("content://" + uiNotificationAuthority + "/uiattachment"); 890 UIPROVIDER_ATTACHMENTS_NOTIFIER = 891 Uri.parse("content://" + uiNotificationAuthority + "/uiattachments"); 892 UIPROVIDER_ALL_ACCOUNTS_NOTIFIER = 893 Uri.parse("content://" + uiNotificationAuthority + "/uiaccts"); 894 UIPROVIDER_MESSAGE_NOTIFIER = 895 Uri.parse("content://" + uiNotificationAuthority + "/uimessage"); 896 UIPROVIDER_RECENT_FOLDERS_NOTIFIER = 897 Uri.parse("content://" + uiNotificationAuthority + "/uirecentfolders"); 898 899 900 // All accounts 901 sURIMatcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT); 902 // A specific account 903 // insert into this URI causes a mailbox to be added to the account 904 sURIMatcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID); 905 sURIMatcher.addURI(EmailContent.AUTHORITY, "accountCheck/#", ACCOUNT_CHECK); 906 907 // Special URI to reset the new message count. Only update works, and values 908 // will be ignored. 909 sURIMatcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount", 910 ACCOUNT_RESET_NEW_COUNT); 911 sURIMatcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount/#", 912 ACCOUNT_RESET_NEW_COUNT_ID); 913 914 // All mailboxes 915 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX); 916 // A specific mailbox 917 // insert into this URI causes a message to be added to the mailbox 918 // ** NOTE For now, the accountKey must be set manually in the values! 919 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox/*", MAILBOX_ID); 920 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxNotification/#", 921 MAILBOX_NOTIFICATION); 922 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxMostRecentMessage/#", 923 MAILBOX_MOST_RECENT_MESSAGE); 924 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxCount/#", MAILBOX_MESSAGE_COUNT); 925 926 // All messages 927 sURIMatcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE); 928 // A specific message 929 // insert into this URI causes an attachment to be added to the message 930 sURIMatcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID); 931 932 // A specific attachment 933 sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT); 934 // A specific attachment (the header information) 935 sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID); 936 // The attachments of a specific message (query only) (insert & delete TBD) 937 sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/message/#", 938 ATTACHMENTS_MESSAGE_ID); 939 sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/cachedFile", 940 ATTACHMENTS_CACHED_FILE_ACCESS); 941 942 // All mail bodies 943 sURIMatcher.addURI(EmailContent.AUTHORITY, "body", BODY); 944 // A specific mail body 945 sURIMatcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID); 946 947 // All hostauth records 948 sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH); 949 // A specific hostauth 950 sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth/*", HOSTAUTH_ID); 951 952 /** 953 * THIS URI HAS SPECIAL SEMANTICS 954 * ITS USE IS INTENDED FOR THE UI TO MARK CHANGES THAT NEED TO BE SYNCED BACK 955 * TO A SERVER VIA A SYNC ADAPTER 956 */ 957 sURIMatcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); 958 sURIMatcher.addURI(EmailContent.AUTHORITY, "messageBySelection", MESSAGE_SELECTION); 959 960 sURIMatcher.addURI(EmailContent.AUTHORITY, MessageMove.PATH, MESSAGE_MOVE); 961 sURIMatcher.addURI(EmailContent.AUTHORITY, MessageStateChange.PATH, 962 MESSAGE_STATE_CHANGE); 963 964 /** 965 * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY 966 * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI 967 * BY THE UI APPLICATION 968 */ 969 // All deleted messages 970 sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE); 971 // A specific deleted message 972 sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID); 973 974 // All updated messages 975 sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE); 976 // A specific updated message 977 sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID); 978 979 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues(); 980 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0); 981 982 sURIMatcher.addURI(EmailContent.AUTHORITY, "policy", POLICY); 983 sURIMatcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID); 984 985 // All quick responses 986 sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE); 987 // A specific quick response 988 sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID); 989 // All quick responses associated with a particular account id 990 sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#", 991 QUICK_RESPONSE_ACCOUNT_ID); 992 993 sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolders/#", UI_FOLDERS); 994 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiallfolders/#", UI_ALL_FOLDERS); 995 sURIMatcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS); 996 sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES); 997 sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE); 998 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiundo", UI_UNDO); 999 sURIMatcher.addURI(EmailContent.AUTHORITY, QUERY_UIREFRESH + "/#", UI_FOLDER_REFRESH); 1000 // We listen to everything trailing uifolder/ since there might be an appVersion 1001 // as in Utils.appendVersionQueryParameter(). 1002 sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolder/*", UI_FOLDER); 1003 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccount/#", UI_ACCOUNT); 1004 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccts", UI_ACCTS); 1005 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachments/#", UI_ATTACHMENTS); 1006 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachment/#", UI_ATTACHMENT); 1007 sURIMatcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH); 1008 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA); 1009 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE); 1010 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION); 1011 sURIMatcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS); 1012 sURIMatcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#", 1013 UI_DEFAULT_RECENT_FOLDERS); 1014 sURIMatcher.addURI(EmailContent.AUTHORITY, "pickTrashFolder/#", 1015 ACCOUNT_PICK_TRASH_FOLDER); 1016 sURIMatcher.addURI(EmailContent.AUTHORITY, "pickSentFolder/#", 1017 ACCOUNT_PICK_SENT_FOLDER); 1018 } 1019 } 1020 1021 /** 1022 * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must 1023 * always be in sync (i.e. there are two database or NO databases). This code will delete 1024 * any "orphan" database, so that both will be created together. Note that an "orphan" database 1025 * will exist after either of the individual databases is deleted due to data corruption. 1026 */ 1027 public void checkDatabases() { 1028 synchronized (sDatabaseLock) { 1029 // Uncache the databases 1030 if (mDatabase != null) { 1031 mDatabase = null; 1032 } 1033 if (mBodyDatabase != null) { 1034 mBodyDatabase = null; 1035 } 1036 // Look for orphans, and delete as necessary; these must always be in sync 1037 final File databaseFile = getContext().getDatabasePath(DATABASE_NAME); 1038 final File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME); 1039 1040 // TODO Make sure attachments are deleted 1041 if (databaseFile.exists() && !bodyFile.exists()) { 1042 LogUtils.w(TAG, "Deleting orphaned EmailProvider database..."); 1043 getContext().deleteDatabase(DATABASE_NAME); 1044 } else if (bodyFile.exists() && !databaseFile.exists()) { 1045 LogUtils.w(TAG, "Deleting orphaned EmailProviderBody database..."); 1046 getContext().deleteDatabase(BODY_DATABASE_NAME); 1047 } 1048 } 1049 } 1050 1051 @Override 1052 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1053 String sortOrder) { 1054 Cursor c = null; 1055 int match; 1056 try { 1057 match = findMatch(uri, "query"); 1058 } catch (IllegalArgumentException e) { 1059 String uriString = uri.toString(); 1060 // If we were passed an illegal uri, see if it ends in /-1 1061 // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor 1062 if (uriString != null && uriString.endsWith("/-1")) { 1063 uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0"); 1064 match = findMatch(uri, "query"); 1065 switch (match) { 1066 case BODY_ID: 1067 case MESSAGE_ID: 1068 case DELETED_MESSAGE_ID: 1069 case UPDATED_MESSAGE_ID: 1070 case ATTACHMENT_ID: 1071 case MAILBOX_ID: 1072 case ACCOUNT_ID: 1073 case HOSTAUTH_ID: 1074 case POLICY_ID: 1075 return new MatrixCursorWithCachedColumns(projection, 0); 1076 } 1077 } 1078 throw e; 1079 } 1080 Context context = getContext(); 1081 // See the comment at delete(), above 1082 SQLiteDatabase db = getDatabase(context); 1083 int table = match >> BASE_SHIFT; 1084 String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT); 1085 String id; 1086 1087 String tableName = TABLE_NAMES.valueAt(table); 1088 1089 try { 1090 switch (match) { 1091 // First, dispatch queries from UnfiedEmail 1092 case UI_SEARCH: 1093 c = uiSearch(uri, projection); 1094 return c; 1095 case UI_ACCTS: 1096 c = uiAccounts(projection); 1097 return c; 1098 case UI_UNDO: 1099 return uiUndo(projection); 1100 case UI_SUBFOLDERS: 1101 case UI_MESSAGES: 1102 case UI_MESSAGE: 1103 case UI_FOLDER: 1104 case UI_ACCOUNT: 1105 case UI_ATTACHMENT: 1106 case UI_ATTACHMENTS: 1107 case UI_CONVERSATION: 1108 case UI_RECENT_FOLDERS: 1109 case UI_ALL_FOLDERS: 1110 // For now, we don't allow selection criteria within these queries 1111 if (selection != null || selectionArgs != null) { 1112 throw new IllegalArgumentException("UI queries can't have selection/args"); 1113 } 1114 1115 final String seenParam = uri.getQueryParameter(UIProvider.SEEN_QUERY_PARAMETER); 1116 final boolean unseenOnly = 1117 seenParam != null && Boolean.FALSE.toString().equals(seenParam); 1118 1119 c = uiQuery(match, uri, projection, unseenOnly); 1120 return c; 1121 case UI_FOLDERS: 1122 c = uiFolders(uri, projection); 1123 return c; 1124 case UI_FOLDER_LOAD_MORE: 1125 c = uiFolderLoadMore(getMailbox(uri)); 1126 return c; 1127 case UI_FOLDER_REFRESH: 1128 c = uiFolderRefresh(getMailbox(uri), 0); 1129 return c; 1130 case MAILBOX_NOTIFICATION: 1131 c = notificationQuery(uri); 1132 return c; 1133 case MAILBOX_MOST_RECENT_MESSAGE: 1134 c = mostRecentMessageQuery(uri); 1135 return c; 1136 case MAILBOX_MESSAGE_COUNT: 1137 c = getMailboxMessageCount(uri); 1138 return c; 1139 case MESSAGE_MOVE: 1140 return db.query(MessageMove.TABLE_NAME, projection, selection, selectionArgs, 1141 null, null, sortOrder, limit); 1142 case MESSAGE_STATE_CHANGE: 1143 return db.query(MessageStateChange.TABLE_NAME, projection, selection, 1144 selectionArgs, null, null, sortOrder, limit); 1145 case BODY: 1146 case MESSAGE: 1147 case UPDATED_MESSAGE: 1148 case DELETED_MESSAGE: 1149 case ATTACHMENT: 1150 case MAILBOX: 1151 case ACCOUNT: 1152 case HOSTAUTH: 1153 case POLICY: 1154 c = db.query(tableName, projection, 1155 selection, selectionArgs, null, null, sortOrder, limit); 1156 break; 1157 case QUICK_RESPONSE: 1158 c = uiQuickResponse(projection); 1159 break; 1160 case BODY_ID: 1161 case MESSAGE_ID: 1162 case DELETED_MESSAGE_ID: 1163 case UPDATED_MESSAGE_ID: 1164 case ATTACHMENT_ID: 1165 case MAILBOX_ID: 1166 case ACCOUNT_ID: 1167 case HOSTAUTH_ID: 1168 case POLICY_ID: 1169 id = uri.getPathSegments().get(1); 1170 c = db.query(tableName, projection, whereWithId(id, selection), 1171 selectionArgs, null, null, sortOrder, limit); 1172 break; 1173 case QUICK_RESPONSE_ID: 1174 id = uri.getPathSegments().get(1); 1175 c = uiQuickResponseId(projection, id); 1176 break; 1177 case ATTACHMENTS_MESSAGE_ID: 1178 // All attachments for the given message 1179 id = uri.getPathSegments().get(2); 1180 c = db.query(Attachment.TABLE_NAME, projection, 1181 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), 1182 selectionArgs, null, null, sortOrder, limit); 1183 break; 1184 case QUICK_RESPONSE_ACCOUNT_ID: 1185 // All quick responses for the given account 1186 id = uri.getPathSegments().get(2); 1187 c = uiQuickResponseAccount(projection, id); 1188 break; 1189 default: 1190 throw new IllegalArgumentException("Unknown URI " + uri); 1191 } 1192 } catch (SQLiteException e) { 1193 checkDatabases(); 1194 throw e; 1195 } catch (RuntimeException e) { 1196 checkDatabases(); 1197 e.printStackTrace(); 1198 throw e; 1199 } finally { 1200 if (c == null) { 1201 // This should never happen, but let's be sure to log it... 1202 // TODO: There are actually cases where c == null is expected, for example 1203 // UI_FOLDER_LOAD_MORE. 1204 // Demoting this to a warning for now until we figure out what to do with it. 1205 LogUtils.w(TAG, "Query returning null for uri: " + uri + ", selection: " 1206 + selection); 1207 } 1208 } 1209 1210 if ((c != null) && !isTemporary()) { 1211 c.setNotificationUri(getContext().getContentResolver(), uri); 1212 } 1213 return c; 1214 } 1215 1216 private static String whereWithId(String id, String selection) { 1217 StringBuilder sb = new StringBuilder(256); 1218 sb.append("_id="); 1219 sb.append(id); 1220 if (selection != null) { 1221 sb.append(" AND ("); 1222 sb.append(selection); 1223 sb.append(')'); 1224 } 1225 return sb.toString(); 1226 } 1227 1228 /** 1229 * Combine a locally-generated selection with a user-provided selection 1230 * 1231 * This introduces risk that the local selection might insert incorrect chars 1232 * into the SQL, so use caution. 1233 * 1234 * @param where locally-generated selection, must not be null 1235 * @param selection user-provided selection, may be null 1236 * @return a single selection string 1237 */ 1238 private static String whereWith(String where, String selection) { 1239 if (selection == null) { 1240 return where; 1241 } 1242 StringBuilder sb = new StringBuilder(where); 1243 sb.append(" AND ("); 1244 sb.append(selection); 1245 sb.append(')'); 1246 1247 return sb.toString(); 1248 } 1249 1250 /** 1251 * Restore a HostAuth from a database, given its unique id 1252 * @param db the database 1253 * @param id the unique id (_id) of the row 1254 * @return a fully populated HostAuth or null if the row does not exist 1255 */ 1256 private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) { 1257 Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION, 1258 HostAuth.RECORD_ID + "=?", new String[] {Long.toString(id)}, null, null, null); 1259 try { 1260 if (c.moveToFirst()) { 1261 HostAuth hostAuth = new HostAuth(); 1262 hostAuth.restore(c); 1263 return hostAuth; 1264 } 1265 return null; 1266 } finally { 1267 c.close(); 1268 } 1269 } 1270 1271 /** 1272 * Copy the Account and HostAuth tables from one database to another 1273 * @param fromDatabase the source database 1274 * @param toDatabase the destination database 1275 * @return the number of accounts copied, or -1 if an error occurred 1276 */ 1277 private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) { 1278 if (fromDatabase == null || toDatabase == null) return -1; 1279 1280 // Lock both databases; for the "from" database, we don't want anyone changing it from 1281 // under us; for the "to" database, we want to make the operation atomic 1282 int copyCount = 0; 1283 fromDatabase.beginTransaction(); 1284 try { 1285 toDatabase.beginTransaction(); 1286 try { 1287 // Delete anything hanging around here 1288 toDatabase.delete(Account.TABLE_NAME, null, null); 1289 toDatabase.delete(HostAuth.TABLE_NAME, null, null); 1290 1291 // Get our account cursor 1292 Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION, 1293 null, null, null, null, null); 1294 if (c == null) return 0; 1295 LogUtils.d(TAG, "fromDatabase accounts: " + c.getCount()); 1296 try { 1297 // Loop through accounts, copying them and associated host auth's 1298 while (c.moveToNext()) { 1299 Account account = new Account(); 1300 account.restore(c); 1301 1302 // Clear security sync key and sync key, as these were specific to the 1303 // state of the account, and we've reset that... 1304 // Clear policy key so that we can re-establish policies from the server 1305 // TODO This is pretty EAS specific, but there's a lot of that around 1306 account.mSecuritySyncKey = null; 1307 account.mSyncKey = null; 1308 account.mPolicyKey = 0; 1309 1310 // Copy host auth's and update foreign keys 1311 HostAuth hostAuth = restoreHostAuth(fromDatabase, 1312 account.mHostAuthKeyRecv); 1313 1314 // The account might have gone away, though very unlikely 1315 if (hostAuth == null) continue; 1316 account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null, 1317 hostAuth.toContentValues()); 1318 1319 // EAS accounts have no send HostAuth 1320 if (account.mHostAuthKeySend > 0) { 1321 hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend); 1322 // Belt and suspenders; I can't imagine that this is possible, 1323 // since we checked the validity of the account above, and the 1324 // database is now locked 1325 if (hostAuth == null) continue; 1326 account.mHostAuthKeySend = toDatabase.insert( 1327 HostAuth.TABLE_NAME, null, hostAuth.toContentValues()); 1328 } 1329 1330 // Now, create the account in the "to" database 1331 toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues()); 1332 copyCount++; 1333 } 1334 } finally { 1335 c.close(); 1336 } 1337 1338 // Say it's ok to commit 1339 toDatabase.setTransactionSuccessful(); 1340 } finally { 1341 toDatabase.endTransaction(); 1342 } 1343 } catch (SQLiteException ex) { 1344 LogUtils.w(TAG, "Exception while copying account tables", ex); 1345 copyCount = -1; 1346 } finally { 1347 fromDatabase.endTransaction(); 1348 } 1349 return copyCount; 1350 } 1351 1352 private static SQLiteDatabase getBackupDatabase(Context context) { 1353 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, BACKUP_DATABASE_NAME); 1354 return helper.getWritableDatabase(); 1355 } 1356 1357 /** 1358 * Backup account data, returning the number of accounts backed up 1359 */ 1360 private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) { 1361 if (MailActivityEmail.DEBUG) { 1362 LogUtils.d(TAG, "backupAccounts..."); 1363 } 1364 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1365 try { 1366 int numBackedUp = copyAccountTables(mainDatabase, backupDatabase); 1367 if (numBackedUp < 0) { 1368 LogUtils.e(TAG, "Account backup failed!"); 1369 } else if (MailActivityEmail.DEBUG) { 1370 LogUtils.d(TAG, "Backed up " + numBackedUp + " accounts..."); 1371 } 1372 return numBackedUp; 1373 } finally { 1374 if (backupDatabase != null) { 1375 backupDatabase.close(); 1376 } 1377 } 1378 } 1379 1380 /** 1381 * Restore account data, returning the number of accounts restored 1382 */ 1383 private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) { 1384 if (MailActivityEmail.DEBUG) { 1385 LogUtils.d(TAG, "restoreAccounts..."); 1386 } 1387 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1388 try { 1389 int numRecovered = copyAccountTables(backupDatabase, mainDatabase); 1390 if (numRecovered > 0) { 1391 LogUtils.e(TAG, "Recovered " + numRecovered + " accounts!"); 1392 } else if (numRecovered < 0) { 1393 LogUtils.e(TAG, "Account recovery failed?"); 1394 } else if (MailActivityEmail.DEBUG) { 1395 LogUtils.d(TAG, "No accounts to restore..."); 1396 } 1397 return numRecovered; 1398 } finally { 1399 if (backupDatabase != null) { 1400 backupDatabase.close(); 1401 } 1402 } 1403 } 1404 1405 private static final String MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX = "insert into %s (" 1406 + MessageChangeLogTable.MESSAGE_KEY + "," + MessageChangeLogTable.SERVER_ID + "," 1407 + MessageChangeLogTable.ACCOUNT_KEY + "," + MessageChangeLogTable.STATUS + ","; 1408 1409 private static final String MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX = ") values (%s, " 1410 + "(select " + Message.SERVER_ID + " from " + Message.TABLE_NAME + " where _id=%s)," 1411 + "(select " + Message.ACCOUNT_KEY + " from " + Message.TABLE_NAME + " where _id=%s)," 1412 + MessageMove.STATUS_NONE_STRING + ","; 1413 1414 /** 1415 * Formatting string to generate the SQL statement for inserting into MessageMove. 1416 * The formatting parameters are: 1417 * table name, message id x 4, destination folder id, message id, destination folder id. 1418 * Duplications are needed for sub-selects. 1419 */ 1420 private static final String MESSAGE_MOVE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX 1421 + MessageMove.SRC_FOLDER_KEY + "," + MessageMove.DST_FOLDER_KEY + "," 1422 + MessageMove.SRC_FOLDER_SERVER_ID + "," + MessageMove.DST_FOLDER_SERVER_ID 1423 + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX 1424 + "(select " + Message.MAILBOX_KEY + " from " + Message.TABLE_NAME + " where _id=%s)," 1425 + "%d," 1426 + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=(select " 1427 + Message.MAILBOX_KEY + " from " + Message.TABLE_NAME + " where _id=%s))," 1428 + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=%d))"; 1429 1430 /** 1431 * Insert a row into the MessageMove table when that message is moved. 1432 * @param db The {@link SQLiteDatabase}. 1433 * @param messageId The id of the message being moved. 1434 * @param dstFolderKey The folder to which the message is being moved. 1435 */ 1436 private void addToMessageMove(final SQLiteDatabase db, final String messageId, 1437 final long dstFolderKey) { 1438 db.execSQL(String.format(Locale.US, MESSAGE_MOVE_INSERT, MessageMove.TABLE_NAME, 1439 messageId, messageId, messageId, messageId, dstFolderKey, messageId, dstFolderKey)); 1440 } 1441 1442 /** 1443 * Formatting string to generate the SQL statement for inserting into MessageStateChange. 1444 * The formatting parameters are: 1445 * table name, message id x 4, new flag read, message id, new flag favorite. 1446 * Duplications are needed for sub-selects. 1447 */ 1448 private static final String MESSAGE_STATE_CHANGE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX 1449 + MessageStateChange.OLD_FLAG_READ + "," + MessageStateChange.NEW_FLAG_READ + "," 1450 + MessageStateChange.OLD_FLAG_FAVORITE + "," + MessageStateChange.NEW_FLAG_FAVORITE 1451 + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX 1452 + "(select " + Message.FLAG_READ + " from " + Message.TABLE_NAME + " where _id=%s)," 1453 + "%d," 1454 + "(select " + Message.FLAG_FAVORITE + " from " + Message.TABLE_NAME + " where _id=%s)," 1455 + "%d)"; 1456 1457 private void addToMessageStateChange(final SQLiteDatabase db, final String messageId, 1458 final int newFlagRead, final int newFlagFavorite) { 1459 db.execSQL(String.format(Locale.US, MESSAGE_STATE_CHANGE_INSERT, 1460 MessageStateChange.TABLE_NAME, messageId, messageId, messageId, messageId, 1461 newFlagRead, messageId, newFlagFavorite)); 1462 } 1463 1464 // select count(*) from (select count(*) as dupes from Mailbox where accountKey=? 1465 // group by serverId) where dupes > 1; 1466 private static final String ACCOUNT_INTEGRITY_SQL = 1467 "select count(*) from (select count(*) as dupes from " + Mailbox.TABLE_NAME + 1468 " where accountKey=? group by " + MailboxColumns.SERVER_ID + ") where dupes > 1"; 1469 1470 1471 // Query to get the protocol for a message. Temporary to switch between new and old upsync 1472 // behavior; should go away when IMAP gets converted. 1473 private static final String GET_PROTOCOL_FOR_MESSAGE = "select h." 1474 + EmailContent.HostAuthColumns.PROTOCOL + " from " 1475 + Message.TABLE_NAME + " m inner join " + Account.TABLE_NAME + " a on m." 1476 + MessageColumns.ACCOUNT_KEY + "=a." + AccountColumns.ID + " inner join " 1477 + HostAuth.TABLE_NAME + " h on a." + AccountColumns.HOST_AUTH_KEY_RECV + "=h." 1478 + EmailContent.HostAuthColumns.ID + " where m." + MessageColumns.ID + "=?"; 1479 1480 @Override 1481 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1482 // Handle this special case the fastest possible way 1483 if (uri == INTEGRITY_CHECK_URI) { 1484 checkDatabases(); 1485 return 0; 1486 } else if (uri == ACCOUNT_BACKUP_URI) { 1487 return backupAccounts(getContext(), getDatabase(getContext())); 1488 } 1489 1490 // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID) 1491 Uri notificationUri = EmailContent.CONTENT_URI; 1492 1493 int match = findMatch(uri, "update"); 1494 Context context = getContext(); 1495 ContentResolver resolver = context.getContentResolver(); 1496 // See the comment at delete(), above 1497 SQLiteDatabase db = getDatabase(context); 1498 int table = match >> BASE_SHIFT; 1499 int result; 1500 1501 // We do NOT allow setting of unreadCount/messageCount via the provider 1502 // These columns are maintained via triggers 1503 if (match == MAILBOX_ID || match == MAILBOX) { 1504 values.remove(MailboxColumns.UNREAD_COUNT); 1505 values.remove(MailboxColumns.MESSAGE_COUNT); 1506 } 1507 1508 String tableName = TABLE_NAMES.valueAt(table); 1509 String id = "0"; 1510 1511 try { 1512 switch (match) { 1513 case ACCOUNT_PICK_TRASH_FOLDER: 1514 return pickTrashFolder(uri); 1515 case ACCOUNT_PICK_SENT_FOLDER: 1516 return pickSentFolder(uri); 1517 case UI_FOLDER: 1518 return uiUpdateFolder(context, uri, values); 1519 case UI_RECENT_FOLDERS: 1520 return uiUpdateRecentFolders(uri, values); 1521 case UI_DEFAULT_RECENT_FOLDERS: 1522 return uiPopulateRecentFolders(uri); 1523 case UI_ATTACHMENT: 1524 return uiUpdateAttachment(uri, values); 1525 case UI_MESSAGE: 1526 return uiUpdateMessage(uri, values); 1527 case ACCOUNT_CHECK: 1528 id = uri.getLastPathSegment(); 1529 // With any error, return 1 (a failure) 1530 int res = 1; 1531 Cursor ic = null; 1532 try { 1533 ic = db.rawQuery(ACCOUNT_INTEGRITY_SQL, new String[] {id}); 1534 if (ic.moveToFirst()) { 1535 res = ic.getInt(0); 1536 } 1537 } finally { 1538 if (ic != null) { 1539 ic.close(); 1540 } 1541 } 1542 // Count of duplicated mailboxes 1543 return res; 1544 case MESSAGE_SELECTION: 1545 Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection, 1546 selectionArgs, null, null, null); 1547 try { 1548 if (findCursor.moveToFirst()) { 1549 return update(ContentUris.withAppendedId( 1550 Message.CONTENT_URI, 1551 findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)), 1552 values, null, null); 1553 } else { 1554 return 0; 1555 } 1556 } finally { 1557 findCursor.close(); 1558 } 1559 case SYNCED_MESSAGE_ID: 1560 case UPDATED_MESSAGE_ID: 1561 case MESSAGE_ID: 1562 case BODY_ID: 1563 case ATTACHMENT_ID: 1564 case MAILBOX_ID: 1565 case ACCOUNT_ID: 1566 case HOSTAUTH_ID: 1567 case QUICK_RESPONSE_ID: 1568 case POLICY_ID: 1569 id = uri.getPathSegments().get(1); 1570 if (match == SYNCED_MESSAGE_ID) { 1571 // TODO: Migrate IMAP to use MessageMove/MessageStateChange as well. 1572 boolean isEas = false; 1573 final Cursor c = db.rawQuery(GET_PROTOCOL_FOR_MESSAGE, new String[] {id}); 1574 if (c != null) { 1575 try { 1576 if (c.moveToFirst()) { 1577 final String protocol = c.getString(0); 1578 isEas = context.getString(R.string.protocol_eas) 1579 .equals(protocol); 1580 } 1581 } finally { 1582 c.close(); 1583 } 1584 } 1585 1586 if (isEas) { 1587 // EAS uses the new upsync classes. 1588 Long dstFolderId = values.getAsLong(MessageColumns.MAILBOX_KEY); 1589 if (dstFolderId != null) { 1590 addToMessageMove(db, id, dstFolderId); 1591 } 1592 Integer flagRead = values.getAsInteger(MessageColumns.FLAG_READ); 1593 Integer flagFavorite = values.getAsInteger(MessageColumns.FLAG_FAVORITE); 1594 int flagReadValue = (flagRead != null) ? 1595 flagRead : MessageStateChange.VALUE_UNCHANGED; 1596 int flagFavoriteValue = (flagFavorite != null) ? 1597 flagFavorite : MessageStateChange.VALUE_UNCHANGED; 1598 if (flagRead != null || flagFavorite != null) { 1599 addToMessageStateChange(db, id, flagReadValue, flagFavoriteValue); 1600 } 1601 } else { 1602 // Old way of doing upsync. 1603 // For synced messages, first copy the old message to the updated table 1604 // Note the insert or ignore semantics, guaranteeing that only the first 1605 // update will be reflected in the updated message table; therefore this 1606 // row will always have the "original" data 1607 db.execSQL(UPDATED_MESSAGE_INSERT + id); 1608 } 1609 } else if (match == MESSAGE_ID) { 1610 db.execSQL(UPDATED_MESSAGE_DELETE + id); 1611 } 1612 result = db.update(tableName, values, whereWithId(id, selection), 1613 selectionArgs); 1614 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 1615 handleMessageUpdateNotifications(uri, id, values); 1616 } else if (match == ATTACHMENT_ID) { 1617 long attId = Integer.parseInt(id); 1618 if (values.containsKey(Attachment.FLAGS)) { 1619 int flags = values.getAsInteger(Attachment.FLAGS); 1620 mAttachmentService.attachmentChanged(context, attId, flags); 1621 } 1622 // Notify UI if necessary; there are only two columns we can change that 1623 // would be worth a notification 1624 if (values.containsKey(AttachmentColumns.UI_STATE) || 1625 values.containsKey(AttachmentColumns.UI_DOWNLOADED_SIZE)) { 1626 // Notify on individual attachment 1627 notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id); 1628 Attachment att = Attachment.restoreAttachmentWithId(context, attId); 1629 if (att != null) { 1630 // And on owning Message 1631 notifyUI(UIPROVIDER_ATTACHMENTS_NOTIFIER, att.mMessageKey); 1632 } 1633 } 1634 } else if (match == MAILBOX_ID) { 1635 notifyUIFolder(id, Mailbox.getAccountIdForMailbox(context, id)); 1636 } else if (match == ACCOUNT_ID) { 1637 updateAccountSyncInterval(Long.parseLong(id), values); 1638 // Notify individual account and "all accounts" 1639 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id); 1640 resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 1641 } 1642 break; 1643 case BODY: 1644 case MESSAGE: 1645 case UPDATED_MESSAGE: 1646 case ATTACHMENT: 1647 case MAILBOX: 1648 case ACCOUNT: 1649 case HOSTAUTH: 1650 case POLICY: 1651 result = db.update(tableName, values, selection, selectionArgs); 1652 break; 1653 1654 case ACCOUNT_RESET_NEW_COUNT_ID: 1655 id = uri.getPathSegments().get(1); 1656 ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 1657 if (values != null) { 1658 Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME); 1659 if (set != null) { 1660 newMessageCount = new ContentValues(); 1661 newMessageCount.put(Account.NEW_MESSAGE_COUNT, set); 1662 } 1663 } 1664 result = db.update(tableName, newMessageCount, 1665 whereWithId(id, selection), selectionArgs); 1666 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1667 break; 1668 case ACCOUNT_RESET_NEW_COUNT: 1669 result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT, 1670 selection, selectionArgs); 1671 // Affects all accounts. Just invalidate all account cache. 1672 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1673 break; 1674 case MESSAGE_MOVE: 1675 result = db.update(MessageMove.TABLE_NAME, values, selection, selectionArgs); 1676 break; 1677 case MESSAGE_STATE_CHANGE: 1678 result = db.update(MessageStateChange.TABLE_NAME, values, selection, 1679 selectionArgs); 1680 break; 1681 default: 1682 throw new IllegalArgumentException("Unknown URI " + uri); 1683 } 1684 } catch (SQLiteException e) { 1685 checkDatabases(); 1686 throw e; 1687 } 1688 1689 // Notify all notifier cursors 1690 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id); 1691 1692 resolver.notifyChange(notificationUri, null); 1693 return result; 1694 } 1695 1696 private void updateSyncStatus(final Bundle extras) { 1697 final long id = extras.getLong(EmailServiceStatus.SYNC_STATUS_ID); 1698 final Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, id); 1699 EmailProvider.this.getContext().getContentResolver().notifyChange(uri, null); 1700 } 1701 1702 @Override 1703 public Bundle call(String method, String arg, Bundle extras) { 1704 LogUtils.d(TAG, "EmailProvider#call(%s, %s)", method, arg); 1705 1706 // Handle sync status callbacks. 1707 if (TextUtils.equals(method, SYNC_STATUS_CALLBACK_METHOD)) { 1708 updateSyncStatus(extras); 1709 return null; 1710 } 1711 1712 // Handle send & save. 1713 final Uri accountUri = Uri.parse(arg); 1714 final long accountId = Long.parseLong(accountUri.getPathSegments().get(1)); 1715 1716 Uri messageUri = null; 1717 if (TextUtils.equals(method, UIProvider.AccountCallMethods.SEND_MESSAGE)) { 1718 messageUri = uiSendDraftMessage(accountId, extras); 1719 Preferences.getPreferences(getContext()).setLastUsedAccountId(accountId); 1720 } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SAVE_MESSAGE)) { 1721 messageUri = uiSaveDraftMessage(accountId, extras); 1722 } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SET_CURRENT_ACCOUNT)) { 1723 LogUtils.d(TAG, "Unhandled (but expected) Content provider method: %s", method); 1724 } else { 1725 LogUtils.wtf(TAG, "Unexpected Content provider method: %s", method); 1726 } 1727 1728 final Bundle result; 1729 if (messageUri != null) { 1730 result = new Bundle(1); 1731 result.putParcelable(UIProvider.MessageColumns.URI, messageUri); 1732 } else { 1733 result = null; 1734 } 1735 1736 return result; 1737 } 1738 1739 @Override 1740 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 1741 if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) { 1742 LogUtils.d(TAG, "EmailProvider.openFile: %s", LogUtils.contentUriToString(TAG, uri)); 1743 } 1744 1745 final int match = findMatch(uri, "openFile"); 1746 switch (match) { 1747 case ATTACHMENTS_CACHED_FILE_ACCESS: 1748 // Parse the cache file path out from the uri 1749 final String cachedFilePath = 1750 uri.getQueryParameter(EmailContent.Attachment.CACHED_FILE_QUERY_PARAM); 1751 1752 if (cachedFilePath != null) { 1753 // clearCallingIdentity means that the download manager will 1754 // check our permissions rather than the permissions of whatever 1755 // code is calling us. 1756 long binderToken = Binder.clearCallingIdentity(); 1757 try { 1758 LogUtils.d(TAG, "Opening attachment %s", cachedFilePath); 1759 return ParcelFileDescriptor.open( 1760 new File(cachedFilePath), ParcelFileDescriptor.MODE_READ_ONLY); 1761 } finally { 1762 Binder.restoreCallingIdentity(binderToken); 1763 } 1764 } 1765 break; 1766 } 1767 1768 throw new FileNotFoundException("unable to open file"); 1769 } 1770 1771 1772 /** 1773 * Returns the base notification URI for the given content type. 1774 * 1775 * @param match The type of content that was modified. 1776 */ 1777 private static Uri getBaseNotificationUri(int match) { 1778 Uri baseUri = null; 1779 switch (match) { 1780 case MESSAGE: 1781 case MESSAGE_ID: 1782 case SYNCED_MESSAGE_ID: 1783 baseUri = Message.NOTIFIER_URI; 1784 break; 1785 case ACCOUNT: 1786 case ACCOUNT_ID: 1787 baseUri = Account.NOTIFIER_URI; 1788 break; 1789 } 1790 return baseUri; 1791 } 1792 1793 /** 1794 * Sends a change notification to any cursors observers of the given base URI. The final 1795 * notification URI is dynamically built to contain the specified information. It will be 1796 * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending 1797 * upon the given values. 1798 * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked. 1799 * If this is necessary, it can be added. However, due to the implementation of 1800 * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications. 1801 * 1802 * @param baseUri The base URI to send notifications to. Must be able to take appended IDs. 1803 * @param op Optional operation to be appended to the URI. 1804 * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be 1805 * appended to the base URI. 1806 */ 1807 private void sendNotifierChange(Uri baseUri, String op, String id) { 1808 if (baseUri == null) return; 1809 1810 final ContentResolver resolver = getContext().getContentResolver(); 1811 1812 // Append the operation, if specified 1813 if (op != null) { 1814 baseUri = baseUri.buildUpon().appendEncodedPath(op).build(); 1815 } 1816 1817 long longId = 0L; 1818 try { 1819 longId = Long.valueOf(id); 1820 } catch (NumberFormatException ignore) {} 1821 if (longId > 0) { 1822 resolver.notifyChange(ContentUris.withAppendedId(baseUri, longId), null); 1823 } else { 1824 resolver.notifyChange(baseUri, null); 1825 } 1826 1827 // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI. 1828 if (baseUri.equals(Message.NOTIFIER_URI)) { 1829 sendMessageListDataChangedNotification(); 1830 } 1831 } 1832 1833 private void sendMessageListDataChangedNotification() { 1834 final Context context = getContext(); 1835 final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED); 1836 // Ideally this intent would contain information about which account changed, to limit the 1837 // updates to that particular account. Unfortunately, that information is not available in 1838 // sendNotifierChange(). 1839 context.sendBroadcast(intent); 1840 } 1841 1842 @Override 1843 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1844 throws OperationApplicationException { 1845 Context context = getContext(); 1846 SQLiteDatabase db = getDatabase(context); 1847 db.beginTransaction(); 1848 try { 1849 ContentProviderResult[] results = super.applyBatch(operations); 1850 db.setTransactionSuccessful(); 1851 return results; 1852 } finally { 1853 db.endTransaction(); 1854 } 1855 } 1856 1857 public static interface AttachmentService { 1858 /** 1859 * Notify the service that an attachment has changed. 1860 */ 1861 void attachmentChanged(Context context, long id, int flags); 1862 } 1863 1864 private final AttachmentService DEFAULT_ATTACHMENT_SERVICE = new AttachmentService() { 1865 @Override 1866 public void attachmentChanged(Context context, long id, int flags) { 1867 // The default implementation delegates to the real service. 1868 AttachmentDownloadService.attachmentChanged(context, id, flags); 1869 } 1870 }; 1871 private final AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE; 1872 1873 private Cursor notificationQuery(final Uri uri) { 1874 final SQLiteDatabase db = getDatabase(getContext()); 1875 final String accountId = uri.getLastPathSegment(); 1876 1877 final StringBuilder sqlBuilder = new StringBuilder(); 1878 sqlBuilder.append("SELECT "); 1879 sqlBuilder.append(MessageColumns.MAILBOX_KEY).append(", "); 1880 sqlBuilder.append("SUM(CASE ") 1881 .append(MessageColumns.FLAG_READ).append(" WHEN 0 THEN 1 ELSE 0 END), "); 1882 sqlBuilder.append("SUM(CASE ") 1883 .append(MessageColumns.FLAG_SEEN).append(" WHEN 0 THEN 1 ELSE 0 END)\n"); 1884 sqlBuilder.append("FROM "); 1885 sqlBuilder.append(Message.TABLE_NAME).append('\n'); 1886 sqlBuilder.append("WHERE "); 1887 sqlBuilder.append(MessageColumns.ACCOUNT_KEY).append(" = ?\n"); 1888 sqlBuilder.append("GROUP BY "); 1889 sqlBuilder.append(MessageColumns.MAILBOX_KEY); 1890 1891 final String sql = sqlBuilder.toString(); 1892 1893 final String[] selectionArgs = {accountId}; 1894 1895 return db.rawQuery(sql, selectionArgs); 1896 } 1897 1898 public Cursor mostRecentMessageQuery(Uri uri) { 1899 SQLiteDatabase db = getDatabase(getContext()); 1900 String mailboxId = uri.getLastPathSegment(); 1901 return db.rawQuery("select max(_id) from Message where mailboxKey=?", 1902 new String[] {mailboxId}); 1903 } 1904 1905 private Cursor getMailboxMessageCount(Uri uri) { 1906 SQLiteDatabase db = getDatabase(getContext()); 1907 String mailboxId = uri.getLastPathSegment(); 1908 return db.rawQuery("select count(*) from Message where mailboxKey=?", 1909 new String[] {mailboxId}); 1910 } 1911 1912 /** 1913 * Support for UnifiedEmail below 1914 */ 1915 1916 private static final String NOT_A_DRAFT_STRING = 1917 Integer.toString(UIProvider.DraftType.NOT_A_DRAFT); 1918 1919 private static final String CONVERSATION_FLAGS = 1920 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE + 1921 ") !=0 THEN " + UIProvider.ConversationFlags.CALENDAR_INVITE + 1922 " ELSE 0 END + " + 1923 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_FORWARDED + 1924 ") !=0 THEN " + UIProvider.ConversationFlags.FORWARDED + 1925 " ELSE 0 END + " + 1926 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_REPLIED_TO + 1927 ") !=0 THEN " + UIProvider.ConversationFlags.REPLIED + 1928 " ELSE 0 END"; 1929 1930 /** 1931 * Array of pre-defined account colors (legacy colors from old email app) 1932 */ 1933 private static final int[] ACCOUNT_COLORS = new int[] { 1934 0xff71aea7, 0xff621919, 0xff18462f, 0xffbf8e52, 0xff001f79, 1935 0xffa8afc2, 0xff6b64c4, 0xff738359, 0xff9d50a4 1936 }; 1937 1938 private static final String CONVERSATION_COLOR = 1939 "@CASE (" + MessageColumns.ACCOUNT_KEY + " - 1) % " + ACCOUNT_COLORS.length + 1940 " WHEN 0 THEN " + ACCOUNT_COLORS[0] + 1941 " WHEN 1 THEN " + ACCOUNT_COLORS[1] + 1942 " WHEN 2 THEN " + ACCOUNT_COLORS[2] + 1943 " WHEN 3 THEN " + ACCOUNT_COLORS[3] + 1944 " WHEN 4 THEN " + ACCOUNT_COLORS[4] + 1945 " WHEN 5 THEN " + ACCOUNT_COLORS[5] + 1946 " WHEN 6 THEN " + ACCOUNT_COLORS[6] + 1947 " WHEN 7 THEN " + ACCOUNT_COLORS[7] + 1948 " WHEN 8 THEN " + ACCOUNT_COLORS[8] + 1949 " END"; 1950 1951 private static final String ACCOUNT_COLOR = 1952 "@CASE (" + AccountColumns.ID + " - 1) % " + ACCOUNT_COLORS.length + 1953 " WHEN 0 THEN " + ACCOUNT_COLORS[0] + 1954 " WHEN 1 THEN " + ACCOUNT_COLORS[1] + 1955 " WHEN 2 THEN " + ACCOUNT_COLORS[2] + 1956 " WHEN 3 THEN " + ACCOUNT_COLORS[3] + 1957 " WHEN 4 THEN " + ACCOUNT_COLORS[4] + 1958 " WHEN 5 THEN " + ACCOUNT_COLORS[5] + 1959 " WHEN 6 THEN " + ACCOUNT_COLORS[6] + 1960 " WHEN 7 THEN " + ACCOUNT_COLORS[7] + 1961 " WHEN 8 THEN " + ACCOUNT_COLORS[8] + 1962 " END"; 1963 /** 1964 * Mapping of UIProvider columns to EmailProvider columns for the message list (called the 1965 * conversation list in UnifiedEmail) 1966 */ 1967 private static ProjectionMap getMessageListMap() { 1968 if (sMessageListMap == null) { 1969 sMessageListMap = ProjectionMap.builder() 1970 .add(BaseColumns._ID, MessageColumns.ID) 1971 .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage")) 1972 .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage")) 1973 .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT) 1974 .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET) 1975 .add(UIProvider.ConversationColumns.CONVERSATION_INFO, null) 1976 .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) 1977 .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) 1978 .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1") 1979 .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0") 1980 .add(UIProvider.ConversationColumns.SENDING_STATE, 1981 Integer.toString(ConversationSendingState.OTHER)) 1982 .add(UIProvider.ConversationColumns.PRIORITY, 1983 Integer.toString(ConversationPriority.LOW)) 1984 .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ) 1985 .add(UIProvider.ConversationColumns.SEEN, MessageColumns.FLAG_SEEN) 1986 .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE) 1987 .add(UIProvider.ConversationColumns.FLAGS, CONVERSATION_FLAGS) 1988 .add(UIProvider.ConversationColumns.ACCOUNT_URI, 1989 uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY)) 1990 .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST) 1991 .build(); 1992 } 1993 return sMessageListMap; 1994 } 1995 private static ProjectionMap sMessageListMap; 1996 1997 /** 1998 * Generate UIProvider draft type; note the test for "reply all" must come before "reply" 1999 */ 2000 private static final String MESSAGE_DRAFT_TYPE = 2001 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_ORIGINAL + 2002 ") !=0 THEN " + UIProvider.DraftType.COMPOSE + 2003 " WHEN (" + MessageColumns.FLAGS + "&" + (1<<20) + 2004 ") !=0 THEN " + UIProvider.DraftType.REPLY_ALL + 2005 " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY + 2006 ") !=0 THEN " + UIProvider.DraftType.REPLY + 2007 " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_FORWARD + 2008 ") !=0 THEN " + UIProvider.DraftType.FORWARD + 2009 " ELSE " + UIProvider.DraftType.NOT_A_DRAFT + " END"; 2010 2011 private static final String MESSAGE_FLAGS = 2012 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE + 2013 ") !=0 THEN " + UIProvider.MessageFlags.CALENDAR_INVITE + 2014 " ELSE 0 END"; 2015 2016 /** 2017 * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in 2018 * UnifiedEmail 2019 */ 2020 private static ProjectionMap getMessageViewMap() { 2021 if (sMessageViewMap == null) { 2022 sMessageViewMap = ProjectionMap.builder() 2023 .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID) 2024 .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID) 2025 .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME)) 2026 .add(UIProvider.MessageColumns.CONVERSATION_ID, 2027 uriWithFQId("uimessage", Message.TABLE_NAME)) 2028 .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT) 2029 .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET) 2030 .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST) 2031 .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST) 2032 .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST) 2033 .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST) 2034 .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST) 2035 .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, 2036 EmailContent.MessageColumns.TIMESTAMP) 2037 .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT) 2038 .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT) 2039 .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0") 2040 .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING) 2041 .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0") 2042 .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, 2043 EmailContent.MessageColumns.FLAG_ATTACHMENT) 2044 .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, 2045 uriWithFQId("uiattachments", Message.TABLE_NAME)) 2046 .add(UIProvider.MessageColumns.MESSAGE_FLAGS, MESSAGE_FLAGS) 2047 .add(UIProvider.MessageColumns.DRAFT_TYPE, MESSAGE_DRAFT_TYPE) 2048 .add(UIProvider.MessageColumns.MESSAGE_ACCOUNT_URI, 2049 uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY)) 2050 .add(UIProvider.MessageColumns.STARRED, EmailContent.MessageColumns.FLAG_FAVORITE) 2051 .add(UIProvider.MessageColumns.READ, EmailContent.MessageColumns.FLAG_READ) 2052 .add(UIProvider.MessageColumns.SEEN, EmailContent.MessageColumns.FLAG_SEEN) 2053 .add(UIProvider.MessageColumns.SPAM_WARNING_STRING, null) 2054 .add(UIProvider.MessageColumns.SPAM_WARNING_LEVEL, 2055 Integer.toString(UIProvider.SpamWarningLevel.NO_WARNING)) 2056 .add(UIProvider.MessageColumns.SPAM_WARNING_LINK_TYPE, 2057 Integer.toString(UIProvider.SpamWarningLinkType.NO_LINK)) 2058 .add(UIProvider.MessageColumns.VIA_DOMAIN, null) 2059 .build(); 2060 } 2061 return sMessageViewMap; 2062 } 2063 private static ProjectionMap sMessageViewMap; 2064 2065 /** 2066 * Generate UIProvider folder capabilities from mailbox flags 2067 */ 2068 private static final String FOLDER_CAPABILITIES = 2069 "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL + 2070 ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES + 2071 " ELSE 0 END"; 2072 2073 /** 2074 * Convert EmailProvider type to UIProvider type 2075 */ 2076 private static final String FOLDER_TYPE = "CASE " + MailboxColumns.TYPE 2077 + " WHEN " + Mailbox.TYPE_INBOX + " THEN " + UIProvider.FolderType.INBOX 2078 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN " + UIProvider.FolderType.DRAFT 2079 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN " + UIProvider.FolderType.OUTBOX 2080 + " WHEN " + Mailbox.TYPE_SENT + " THEN " + UIProvider.FolderType.SENT 2081 + " WHEN " + Mailbox.TYPE_TRASH + " THEN " + UIProvider.FolderType.TRASH 2082 + " WHEN " + Mailbox.TYPE_JUNK + " THEN " + UIProvider.FolderType.SPAM 2083 + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + UIProvider.FolderType.STARRED 2084 + " WHEN " + Mailbox.TYPE_UNREAD + " THEN " + UIProvider.FolderType.UNREAD 2085 + " WHEN " + Mailbox.TYPE_SEARCH + " THEN " 2086 + getFolderTypeFromMailboxType(Mailbox.TYPE_SEARCH) 2087 + " ELSE " + UIProvider.FolderType.DEFAULT + " END"; 2088 2089 private static final String FOLDER_ICON = "CASE " + MailboxColumns.TYPE 2090 + " WHEN " + Mailbox.TYPE_INBOX + " THEN " + R.drawable.ic_folder_inbox 2091 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN " + R.drawable.ic_folder_drafts 2092 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN " + R.drawable.ic_folder_outbox 2093 + " WHEN " + Mailbox.TYPE_SENT + " THEN " + R.drawable.ic_folder_sent 2094 + " WHEN " + Mailbox.TYPE_TRASH + " THEN " + R.drawable.ic_folder_trash 2095 + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + R.drawable.ic_folder_star 2096 + " ELSE -1 END"; 2097 2098 /** 2099 * Local-only folders set totalCount < 0; such folders should substitute message count for 2100 * total count. 2101 * TODO: IMAP and POP don't adhere to this convention yet so for now we force a few types. 2102 */ 2103 private static final String TOTAL_COUNT = "CASE WHEN " 2104 + MailboxColumns.TOTAL_COUNT + "<0 OR " 2105 + MailboxColumns.TYPE + "=" + Mailbox.TYPE_DRAFTS + " OR " 2106 + MailboxColumns.TYPE + "=" + Mailbox.TYPE_OUTBOX + " OR " 2107 + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH 2108 + " THEN " + MailboxColumns.MESSAGE_COUNT 2109 + " ELSE " + MailboxColumns.TOTAL_COUNT + " END"; 2110 2111 private static ProjectionMap getFolderListMap() { 2112 if (sFolderListMap == null) { 2113 sFolderListMap = ProjectionMap.builder() 2114 .add(BaseColumns._ID, MailboxColumns.ID) 2115 .add(UIProvider.FolderColumns.PERSISTENT_ID, MailboxColumns.SERVER_ID) 2116 .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) 2117 .add(UIProvider.FolderColumns.NAME, "displayName") 2118 .add(UIProvider.FolderColumns.HAS_CHILDREN, 2119 MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN) 2120 .add(UIProvider.FolderColumns.CAPABILITIES, FOLDER_CAPABILITIES) 2121 .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") 2122 .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) 2123 .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders")) 2124 .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT) 2125 .add(UIProvider.FolderColumns.TOTAL_COUNT, TOTAL_COUNT) 2126 .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId(QUERY_UIREFRESH)) 2127 .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS) 2128 .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT) 2129 .add(UIProvider.FolderColumns.TYPE, FOLDER_TYPE) 2130 .add(UIProvider.FolderColumns.ICON_RES_ID, FOLDER_ICON) 2131 .add(UIProvider.FolderColumns.HIERARCHICAL_DESC, MailboxColumns.HIERARCHICAL_NAME) 2132 .add(UIProvider.FolderColumns.PARENT_URI, "case when " + MailboxColumns.PARENT_KEY 2133 + "=" + Mailbox.NO_MAILBOX + " then NULL else " + 2134 uriWithColumn("uifolder", MailboxColumns.PARENT_KEY) + " end") 2135 .build(); 2136 } 2137 return sFolderListMap; 2138 } 2139 private static ProjectionMap sFolderListMap; 2140 2141 /** 2142 * Constructs the map of default entries for accounts. These values can be overridden in 2143 * {@link #genQueryAccount(String[], String)}. 2144 */ 2145 private static ProjectionMap getAccountListMap(Context context) { 2146 if (sAccountListMap == null) { 2147 final MailPrefs mailPrefs = MailPrefs.get(context); 2148 2149 final ProjectionMap.Builder builder = ProjectionMap.builder() 2150 .add(BaseColumns._ID, AccountColumns.ID) 2151 .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders")) 2152 .add(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, uriWithId("uiallfolders")) 2153 .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME) 2154 .add(UIProvider.AccountColumns.UNDO_URI, 2155 ("'content://" + EmailContent.AUTHORITY + "/uiundo'")) 2156 .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount")) 2157 .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch")) 2158 // TODO: Is provider version used? 2159 .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1") 2160 .add(UIProvider.AccountColumns.SYNC_STATUS, "0") 2161 .add(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI, 2162 uriWithId("uirecentfolders")) 2163 .add(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI, 2164 uriWithId("uidefaultrecentfolders")) 2165 .add(UIProvider.AccountColumns.SettingsColumns.SIGNATURE, 2166 AccountColumns.SIGNATURE) 2167 .add(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS, 2168 Integer.toString(UIProvider.SnapHeaderValue.ALWAYS)) 2169 .add(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR, 2170 Integer.toString(mailPrefs.getDefaultReplyAll() 2171 ? UIProvider.DefaultReplyBehavior.REPLY_ALL 2172 : UIProvider.DefaultReplyBehavior.REPLY)) 2173 .add(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, "0") 2174 .add(UIProvider.AccountColumns.SettingsColumns.CONVERSATION_VIEW_MODE, 2175 Integer.toString(UIProvider.ConversationViewMode.UNDEFINED)) 2176 .add(UIProvider.AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN, null); 2177 2178 final String feedbackUri = context.getString(R.string.email_feedback_uri); 2179 if (!TextUtils.isEmpty(feedbackUri)) { 2180 // This string needs to be in single quotes, as it will be used as a constant 2181 // in a sql expression 2182 builder.add(UIProvider.AccountColumns.SEND_FEEDBACK_INTENT_URI, 2183 "'" + feedbackUri + "'"); 2184 } 2185 2186 sAccountListMap = builder.build(); 2187 } 2188 return sAccountListMap; 2189 } 2190 private static ProjectionMap sAccountListMap; 2191 2192 private static ProjectionMap getQuickResponseMap() { 2193 if (sQuickResponseMap == null) { 2194 sQuickResponseMap = ProjectionMap.builder() 2195 .add(UIProvider.QuickResponseColumns.TEXT, 2196 EmailContent.QuickResponseColumns.TEXT) 2197 .add(UIProvider.QuickResponseColumns.URI, 2198 "'" + combinedUriString("quickresponse", "") + "'||" 2199 + EmailContent.QuickResponseColumns.ID) 2200 .build(); 2201 } 2202 return sQuickResponseMap; 2203 } 2204 private static ProjectionMap sQuickResponseMap; 2205 2206 /** 2207 * The "ORDER BY" clause for top level folders 2208 */ 2209 private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE 2210 + " WHEN " + Mailbox.TYPE_INBOX + " THEN 0" 2211 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN 1" 2212 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN 2" 2213 + " WHEN " + Mailbox.TYPE_SENT + " THEN 3" 2214 + " WHEN " + Mailbox.TYPE_TRASH + " THEN 4" 2215 + " WHEN " + Mailbox.TYPE_JUNK + " THEN 5" 2216 // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order. 2217 + " ELSE 10 END" 2218 + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 2219 2220 /** 2221 * Mapping of UIProvider columns to EmailProvider columns for a message's attachments 2222 */ 2223 private static ProjectionMap getAttachmentMap() { 2224 if (sAttachmentMap == null) { 2225 sAttachmentMap = ProjectionMap.builder() 2226 .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME) 2227 .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE) 2228 .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment")) 2229 .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE) 2230 .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE) 2231 .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION) 2232 .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE, 2233 AttachmentColumns.UI_DOWNLOADED_SIZE) 2234 .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI) 2235 .build(); 2236 } 2237 return sAttachmentMap; 2238 } 2239 private static ProjectionMap sAttachmentMap; 2240 2241 /** 2242 * Generate the SELECT clause using a specified mapping and the original UI projection 2243 * @param map the ProjectionMap to use for this projection 2244 * @param projection the projection as sent by UnifiedEmail 2245 * @return a StringBuilder containing the SELECT expression for a SQLite query 2246 */ 2247 private static StringBuilder genSelect(ProjectionMap map, String[] projection) { 2248 return genSelect(map, projection, EMPTY_CONTENT_VALUES); 2249 } 2250 2251 private static StringBuilder genSelect(ProjectionMap map, String[] projection, 2252 ContentValues values) { 2253 final StringBuilder sb = new StringBuilder("SELECT "); 2254 boolean first = true; 2255 for (final String column: projection) { 2256 if (first) { 2257 first = false; 2258 } else { 2259 sb.append(','); 2260 } 2261 final String val; 2262 // First look at values; this is an override of default behavior 2263 if (values.containsKey(column)) { 2264 final String value = values.getAsString(column); 2265 if (value == null) { 2266 val = "NULL AS " + column; 2267 } else if (value.startsWith("@")) { 2268 val = value.substring(1) + " AS " + column; 2269 } else { 2270 val = "'" + value + "' AS " + column; 2271 } 2272 } else { 2273 // Now, get the standard value for the column from our projection map 2274 final String mapVal = map.get(column); 2275 // If we don't have the column, return "NULL AS <column>", and warn 2276 if (mapVal == null) { 2277 val = "NULL AS " + column; 2278 // Apparently there's a lot of these, so don't spam the log with warnings 2279 // LogUtils.w(TAG, "column " + column + " missing from projection map"); 2280 } else { 2281 val = mapVal; 2282 } 2283 } 2284 sb.append(val); 2285 } 2286 return sb; 2287 } 2288 2289 /** 2290 * Convenience method to create a Uri string given the "type" of query; we append the type 2291 * of the query and the id column name (_id) 2292 * 2293 * @param type the "type" of the query, as defined by our UriMatcher definitions 2294 * @return a Uri string 2295 */ 2296 private static String uriWithId(String type) { 2297 return uriWithColumn(type, EmailContent.RECORD_ID); 2298 } 2299 2300 /** 2301 * Convenience method to create a Uri string given the "type" of query; we append the type 2302 * of the query and the passed in column name 2303 * 2304 * @param type the "type" of the query, as defined by our UriMatcher definitions 2305 * @param columnName the column in the table being queried 2306 * @return a Uri string 2307 */ 2308 private static String uriWithColumn(String type, String columnName) { 2309 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + columnName; 2310 } 2311 2312 /** 2313 * Convenience method to create a Uri string given the "type" of query and the table name to 2314 * which it applies; we append the type of the query and the fully qualified (FQ) id column 2315 * (i.e. including the table name); we need this for join queries where _id would otherwise 2316 * be ambiguous 2317 * 2318 * @param type the "type" of the query, as defined by our UriMatcher definitions 2319 * @param tableName the name of the table whose _id is referred to 2320 * @return a Uri string 2321 */ 2322 private static String uriWithFQId(String type, String tableName) { 2323 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id"; 2324 } 2325 2326 // Regex that matches start of img tag. '<(?i)img\s+'. 2327 private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+"); 2328 2329 /** 2330 * Class that holds the sqlite query and the attachment (JSON) value (which might be null) 2331 */ 2332 private static class MessageQuery { 2333 final String query; 2334 final String attachmentJson; 2335 2336 MessageQuery(String _query, String _attachmentJson) { 2337 query = _query; 2338 attachmentJson = _attachmentJson; 2339 } 2340 } 2341 2342 /** 2343 * Generate the "view message" SQLite query, given a projection from UnifiedEmail 2344 * 2345 * @param uiProjection as passed from UnifiedEmail 2346 * @return the SQLite query to be executed on the EmailProvider database 2347 */ 2348 private MessageQuery genQueryViewMessage(String[] uiProjection, String id) { 2349 Context context = getContext(); 2350 long messageId = Long.parseLong(id); 2351 Message msg = Message.restoreMessageWithId(context, messageId); 2352 ContentValues values = new ContentValues(); 2353 String attachmentJson = null; 2354 if (msg != null) { 2355 Body body = Body.restoreBodyWithMessageId(context, messageId); 2356 if (body != null) { 2357 if (body.mHtmlContent != null) { 2358 if (IMG_TAG_START_REGEX.matcher(body.mHtmlContent).find()) { 2359 values.put(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, 1); 2360 } 2361 } 2362 } 2363 Address[] fromList = Address.unpack(msg.mFrom); 2364 int autoShowImages = 0; 2365 final MailPrefs mailPrefs = MailPrefs.get(context); 2366 for (Address sender : fromList) { 2367 final String email = sender.getAddress(); 2368 if (mailPrefs.getDisplayImagesFromSender(email)) { 2369 autoShowImages = 1; 2370 break; 2371 } 2372 } 2373 values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, autoShowImages); 2374 // Add attachments... 2375 Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId); 2376 if (atts.length > 0) { 2377 ArrayList<com.android.mail.providers.Attachment> uiAtts = 2378 new ArrayList<com.android.mail.providers.Attachment>(); 2379 for (Attachment att : atts) { 2380 if (att.mContentId != null && att.getContentUri() != null) { 2381 continue; 2382 } 2383 com.android.mail.providers.Attachment uiAtt = 2384 new com.android.mail.providers.Attachment(); 2385 uiAtt.setName(att.mFileName); 2386 uiAtt.setContentType(att.mMimeType); 2387 uiAtt.size = (int) att.mSize; 2388 uiAtt.uri = uiUri("uiattachment", att.mId); 2389 uiAtts.add(uiAtt); 2390 } 2391 values.put(UIProvider.MessageColumns.ATTACHMENTS, "@?"); // @ for literal 2392 attachmentJson = com.android.mail.providers.Attachment.toJSONArray(uiAtts); 2393 } 2394 if (msg.mDraftInfo != 0) { 2395 values.put(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, 2396 (msg.mDraftInfo & Message.DRAFT_INFO_APPEND_REF_MESSAGE) != 0 ? 1 : 0); 2397 values.put(UIProvider.MessageColumns.QUOTE_START_POS, 2398 msg.mDraftInfo & Message.DRAFT_INFO_QUOTE_POS_MASK); 2399 } 2400 if ((msg.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) { 2401 values.put(UIProvider.MessageColumns.EVENT_INTENT_URI, 2402 "content://ui.email2.android.com/event/" + msg.mId); 2403 } 2404 } 2405 StringBuilder sb = genSelect(getMessageViewMap(), uiProjection, values); 2406 sb.append(" FROM " + Message.TABLE_NAME + " LEFT JOIN " + Body.TABLE_NAME + " ON " + 2407 Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " WHERE " + 2408 Message.TABLE_NAME + "." + Message.RECORD_ID + "=?"); 2409 String sql = sb.toString(); 2410 return new MessageQuery(sql, attachmentJson); 2411 } 2412 2413 private static void appendConversationInfoColumns(final StringBuilder stringBuilder) { 2414 // TODO(skennedy) These columns are needed for the respond call for ConversationInfo :( 2415 // There may be a better way to do this, but since the projection is specified by the 2416 // unified UI code, it can't ask for these columns. 2417 stringBuilder.append(',').append(MessageColumns.DISPLAY_NAME) 2418 .append(',').append(MessageColumns.FROM_LIST); 2419 } 2420 2421 /** 2422 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 2423 * 2424 * @param uiProjection as passed from UnifiedEmail 2425 * @param unseenOnly <code>true</code> to only return unseen messages 2426 * @return the SQLite query to be executed on the EmailProvider database 2427 */ 2428 private static String genQueryMailboxMessages(String[] uiProjection, final boolean unseenOnly) { 2429 StringBuilder sb = genSelect(getMessageListMap(), uiProjection); 2430 appendConversationInfoColumns(sb); 2431 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2432 Message.FLAG_LOADED_SELECTION + " AND " + 2433 Message.MAILBOX_KEY + "=? "); 2434 if (unseenOnly) { 2435 sb.append("AND ").append(MessageColumns.FLAG_SEEN).append(" = 0 "); 2436 } 2437 sb.append("ORDER BY " + MessageColumns.TIMESTAMP + " DESC "); 2438 sb.append("LIMIT " + UIProvider.CONVERSATION_PROJECTION_QUERY_CURSOR_WINDOW_LIMT); 2439 return sb.toString(); 2440 } 2441 2442 /** 2443 * Generate various virtual mailbox SQLite queries, given a projection from UnifiedEmail 2444 * 2445 * @param uiProjection as passed from UnifiedEmail 2446 * @param mailboxId the id of the virtual mailbox 2447 * @param unseenOnly <code>true</code> to only return unseen messages 2448 * @return the SQLite query to be executed on the EmailProvider database 2449 */ 2450 private static Cursor getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection, 2451 long mailboxId, final boolean unseenOnly) { 2452 ContentValues values = new ContentValues(); 2453 values.put(UIProvider.ConversationColumns.COLOR, CONVERSATION_COLOR); 2454 final int virtualMailboxId = getVirtualMailboxType(mailboxId); 2455 final String[] selectionArgs; 2456 StringBuilder sb = genSelect(getMessageListMap(), uiProjection, values); 2457 appendConversationInfoColumns(sb); 2458 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2459 Message.FLAG_LOADED_SELECTION + " AND "); 2460 if (isCombinedMailbox(mailboxId)) { 2461 if (unseenOnly) { 2462 sb.append(MessageColumns.FLAG_SEEN).append("=0 AND "); 2463 } 2464 selectionArgs = null; 2465 } else { 2466 if (virtualMailboxId == Mailbox.TYPE_INBOX) { 2467 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); 2468 } 2469 sb.append(MessageColumns.ACCOUNT_KEY).append("=? AND "); 2470 selectionArgs = new String[]{getVirtualMailboxAccountIdString(mailboxId)}; 2471 } 2472 switch (getVirtualMailboxType(mailboxId)) { 2473 case Mailbox.TYPE_INBOX: 2474 sb.append(MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID + 2475 " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + 2476 "=" + Mailbox.TYPE_INBOX + ")"); 2477 break; 2478 case Mailbox.TYPE_STARRED: 2479 sb.append(MessageColumns.FLAG_FAVORITE + "=1"); 2480 break; 2481 case Mailbox.TYPE_UNREAD: 2482 sb.append(MessageColumns.FLAG_READ + "=0 AND " + MessageColumns.MAILBOX_KEY + 2483 " NOT IN (SELECT " + MailboxColumns.ID + " FROM " + Mailbox.TABLE_NAME + 2484 " WHERE " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH + ")"); 2485 break; 2486 default: 2487 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); 2488 } 2489 sb.append(" ORDER BY " + MessageColumns.TIMESTAMP + " DESC"); 2490 return db.rawQuery(sb.toString(), selectionArgs); 2491 } 2492 2493 /** 2494 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 2495 * 2496 * @param uiProjection as passed from UnifiedEmail 2497 * @return the SQLite query to be executed on the EmailProvider database 2498 */ 2499 private static String genQueryConversation(String[] uiProjection) { 2500 StringBuilder sb = genSelect(getMessageListMap(), uiProjection); 2501 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.RECORD_ID + "=?"); 2502 return sb.toString(); 2503 } 2504 2505 /** 2506 * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail 2507 * 2508 * @param uiProjection as passed from UnifiedEmail 2509 * @return the SQLite query to be executed on the EmailProvider database 2510 */ 2511 private static String genQueryAccountMailboxes(String[] uiProjection) { 2512 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 2513 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2514 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2515 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + 2516 " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY "); 2517 sb.append(MAILBOX_ORDER_BY); 2518 return sb.toString(); 2519 } 2520 2521 /** 2522 * Generate the "all folders" SQLite query, given a projection from UnifiedEmail. The list is 2523 * sorted by the name as it appears in a hierarchical listing 2524 * 2525 * @param uiProjection as passed from UnifiedEmail 2526 * @return the SQLite query to be executed on the EmailProvider database 2527 */ 2528 private static String genQueryAccountAllMailboxes(String[] uiProjection) { 2529 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 2530 // Use a derived column to choose either hierarchicalName or displayName 2531 sb.append(", case when " + MailboxColumns.HIERARCHICAL_NAME + " is null then " + 2532 MailboxColumns.DISPLAY_NAME + " else " + MailboxColumns.HIERARCHICAL_NAME + 2533 " end as h_name"); 2534 // Order by the derived column 2535 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2536 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2537 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + 2538 " ORDER BY h_name"); 2539 return sb.toString(); 2540 } 2541 2542 /** 2543 * Generate the "recent folder list" SQLite query, given a projection from UnifiedEmail 2544 * 2545 * @param uiProjection as passed from UnifiedEmail 2546 * @return the SQLite query to be executed on the EmailProvider database 2547 */ 2548 private static String genQueryRecentMailboxes(String[] uiProjection) { 2549 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 2550 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2551 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2552 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + 2553 " AND " + MailboxColumns.PARENT_KEY + " < 0 AND " + 2554 MailboxColumns.LAST_TOUCHED_TIME + " > 0 ORDER BY " + 2555 MailboxColumns.LAST_TOUCHED_TIME + " DESC"); 2556 return sb.toString(); 2557 } 2558 2559 private static int getFolderCapabilities(EmailServiceInfo info, int flags, int type, 2560 long mailboxId) { 2561 // All folders support delete, except drafts. 2562 int caps = 0; 2563 if (type != Mailbox.TYPE_DRAFTS) { 2564 caps = UIProvider.FolderCapabilities.DELETE; 2565 } 2566 if (info != null && info.offerLookback) { 2567 // Protocols supporting lookback support settings 2568 caps |= UIProvider.FolderCapabilities.SUPPORTS_SETTINGS; 2569 } 2570 if ((flags & Mailbox.FLAG_ACCEPTS_MOVED_MAIL) != 0) { 2571 // If the mailbox can accept moved mail, report that as well 2572 caps |= UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES; 2573 caps |= UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION; 2574 } 2575 2576 // For trash, we don't allow undo 2577 if (type == Mailbox.TYPE_TRASH) { 2578 caps = UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES | 2579 UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION | 2580 UIProvider.FolderCapabilities.DELETE | 2581 UIProvider.FolderCapabilities.DELETE_ACTION_FINAL; 2582 } 2583 if (isVirtualMailbox(mailboxId)) { 2584 caps |= UIProvider.FolderCapabilities.IS_VIRTUAL; 2585 } 2586 return caps; 2587 } 2588 2589 /** 2590 * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail 2591 * 2592 * @param uiProjection as passed from UnifiedEmail 2593 * @return the SQLite query to be executed on the EmailProvider database 2594 */ 2595 private String genQueryMailbox(String[] uiProjection, String id) { 2596 long mailboxId = Long.parseLong(id); 2597 ContentValues values = new ContentValues(); 2598 if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) { 2599 // This is the current search mailbox; use the total count 2600 values = new ContentValues(); 2601 values.put(UIProvider.FolderColumns.TOTAL_COUNT, mSearchParams.mTotalCount); 2602 // "load more" is valid for search results 2603 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 2604 uiUriString("uiloadmore", mailboxId)); 2605 values.put(UIProvider.FolderColumns.CAPABILITIES, UIProvider.FolderCapabilities.DELETE); 2606 } else { 2607 Context context = getContext(); 2608 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 2609 // Make sure we can't get NPE if mailbox has disappeared (the result will end up moot) 2610 if (mailbox != null) { 2611 String protocol = Account.getProtocol(context, mailbox.mAccountKey); 2612 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 2613 // All folders support delete 2614 if (info != null && info.offerLoadMore) { 2615 // "load more" is valid for protocols not supporting "lookback" 2616 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 2617 uiUriString("uiloadmore", mailboxId)); 2618 } 2619 values.put(UIProvider.FolderColumns.CAPABILITIES, 2620 getFolderCapabilities(info, mailbox.mFlags, mailbox.mType, mailboxId)); 2621 // The persistent id is used to form a filename, so we must ensure that it doesn't 2622 // include illegal characters (such as '/'). Only perform the encoding if this 2623 // query wants the persistent id. 2624 boolean shouldEncodePersistentId = false; 2625 if (uiProjection == null) { 2626 shouldEncodePersistentId = true; 2627 } else { 2628 for (final String column : uiProjection) { 2629 if (TextUtils.equals(column, UIProvider.FolderColumns.PERSISTENT_ID)) { 2630 shouldEncodePersistentId = true; 2631 break; 2632 } 2633 } 2634 } 2635 if (shouldEncodePersistentId) { 2636 values.put(UIProvider.FolderColumns.PERSISTENT_ID, 2637 Base64.encodeToString(mailbox.mServerId.getBytes(), 2638 Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING)); 2639 } 2640 } 2641 } 2642 StringBuilder sb = genSelect(getFolderListMap(), uiProjection, values); 2643 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ID + "=?"); 2644 return sb.toString(); 2645 } 2646 2647 public static final String LEGACY_AUTHORITY = "ui.email.android.com"; 2648 private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://" + LEGACY_AUTHORITY); 2649 2650 private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com"); 2651 2652 private static String getExternalUriString(String segment, String account) { 2653 return BASE_EXTERNAL_URI.buildUpon().appendPath(segment) 2654 .appendQueryParameter("account", account).build().toString(); 2655 } 2656 2657 private static String getExternalUriStringEmail2(String segment, String account) { 2658 return BASE_EXTERAL_URI2.buildUpon().appendPath(segment) 2659 .appendQueryParameter("account", account).build().toString(); 2660 } 2661 2662 private static String getBits(int bitField) { 2663 StringBuilder sb = new StringBuilder(" "); 2664 for (int i = 0; i < 32; i++, bitField >>= 1) { 2665 if ((bitField & 1) != 0) { 2666 sb.append(i) 2667 .append(" "); 2668 } 2669 } 2670 return sb.toString(); 2671 } 2672 2673 private static int getCapabilities(Context context, long accountId) { 2674 final EmailServiceProxy service = 2675 EmailServiceUtils.getServiceForAccount(context, accountId); 2676 int capabilities = 0; 2677 Account acct = null; 2678 try { 2679 service.setTimeout(10); 2680 acct = Account.restoreAccountWithId(context, accountId); 2681 if (acct == null) { 2682 LogUtils.d(TAG, "getCapabilities() for " + accountId 2683 + ": returning 0x0 (no account)"); 2684 return 0; 2685 } 2686 capabilities = service.getCapabilities(acct); 2687 LogUtils.d(TAG, "getCapabilities() for %s: 0x%x %s", 2688 LogUtils.sanitizeAccountName(acct.mDisplayName), 2689 capabilities, getBits(capabilities)); 2690 } catch (RemoteException e) { 2691 // Nothing to do 2692 LogUtils.w(TAG, "getCapabilities() for " + acct.mDisplayName + ": RemoteException"); 2693 } 2694 2695 // If the configuration states that feedback is supported, add that capability 2696 final Resources res = context.getResources(); 2697 if (res.getBoolean(R.bool.feedback_supported)) { 2698 capabilities |= UIProvider.AccountCapabilities.SEND_FEEDBACK; 2699 } 2700 2701 capabilities |= UIProvider.AccountCapabilities.NESTED_FOLDERS; 2702 2703 return capabilities; 2704 } 2705 2706 /** 2707 * Generate a "single account" SQLite query, given a projection from UnifiedEmail 2708 * 2709 * @param uiProjection as passed from UnifiedEmail 2710 * @param id account row ID 2711 * @return the SQLite query to be executed on the EmailProvider database 2712 */ 2713 private String genQueryAccount(String[] uiProjection, String id) { 2714 final ContentValues values = new ContentValues(); 2715 final long accountId = Long.parseLong(id); 2716 final Context context = getContext(); 2717 2718 EmailServiceInfo info = null; 2719 2720 // TODO: If uiProjection is null, this will NPE. We should do everything here if it's null. 2721 final Set<String> projectionColumns = ImmutableSet.copyOf(uiProjection); 2722 2723 if (projectionColumns.contains(UIProvider.AccountColumns.CAPABILITIES)) { 2724 // Get account capabilities from the service 2725 values.put(UIProvider.AccountColumns.CAPABILITIES, getCapabilities(context, accountId)); 2726 } 2727 if (projectionColumns.contains(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) { 2728 values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI, 2729 getExternalUriString("settings", id)); 2730 } 2731 if (projectionColumns.contains(UIProvider.AccountColumns.COMPOSE_URI)) { 2732 values.put(UIProvider.AccountColumns.COMPOSE_URI, 2733 getExternalUriStringEmail2("compose", id)); 2734 } 2735 if (projectionColumns.contains(UIProvider.AccountColumns.MIME_TYPE)) { 2736 values.put(UIProvider.AccountColumns.MIME_TYPE, EMAIL_APP_MIME_TYPE); 2737 } 2738 if (projectionColumns.contains(UIProvider.AccountColumns.COLOR)) { 2739 values.put(UIProvider.AccountColumns.COLOR, ACCOUNT_COLOR); 2740 } 2741 2742 final Preferences prefs = Preferences.getPreferences(getContext()); 2743 final MailPrefs mailPrefs = MailPrefs.get(getContext()); 2744 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) { 2745 values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE, 2746 prefs.getConfirmDelete() ? "1" : "0"); 2747 } 2748 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) { 2749 values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND, 2750 prefs.getConfirmSend() ? "1" : "0"); 2751 } 2752 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.SWIPE)) { 2753 values.put(UIProvider.AccountColumns.SettingsColumns.SWIPE, 2754 mailPrefs.getConversationListSwipeActionInteger(false)); 2755 } 2756 if (projectionColumns.contains( 2757 UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) { 2758 values.put(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON, 2759 getConversationListIcon(mailPrefs)); 2760 } 2761 if (projectionColumns.contains( 2762 UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS)) { 2763 values.put(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS, 2764 "0"); 2765 } 2766 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) { 2767 int autoAdvance = prefs.getAutoAdvanceDirection(); 2768 values.put(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE, 2769 autoAdvanceToUiValue(autoAdvance)); 2770 } 2771 if (projectionColumns.contains( 2772 UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)) { 2773 int textZoom = prefs.getTextZoom(); 2774 values.put(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE, 2775 textZoomToUiValue(textZoom)); 2776 } 2777 // Set default inbox, if we've got an inbox; otherwise, say initial sync needed 2778 final long inboxMailboxId = 2779 Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_INBOX); 2780 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX) && 2781 inboxMailboxId != Mailbox.NO_MAILBOX) { 2782 values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX, 2783 uiUriString("uifolder", inboxMailboxId)); 2784 } 2785 if (projectionColumns.contains( 2786 UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME) && 2787 inboxMailboxId != Mailbox.NO_MAILBOX) { 2788 values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME, 2789 Mailbox.getDisplayName(context, inboxMailboxId)); 2790 } 2791 if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_STATUS)) { 2792 if (inboxMailboxId != Mailbox.NO_MAILBOX) { 2793 values.put(UIProvider.AccountColumns.SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC); 2794 } else { 2795 values.put(UIProvider.AccountColumns.SYNC_STATUS, 2796 UIProvider.SyncStatus.INITIAL_SYNC_NEEDED); 2797 } 2798 } 2799 if (projectionColumns.contains( 2800 UIProvider.AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED)) { 2801 // Email doesn't support priority inbox, so always state priority arrows disabled. 2802 values.put(UIProvider.AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED, "0"); 2803 } 2804 if (projectionColumns.contains( 2805 UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI)) { 2806 // Set the setup intent if needed 2807 // TODO We should clarify/document the trash/setup relationship 2808 long trashId = Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_TRASH); 2809 if (trashId == Mailbox.NO_MAILBOX) { 2810 info = EmailServiceUtils.getServiceInfoForAccount(context, accountId); 2811 if (info != null && info.requiresSetup) { 2812 values.put(UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI, 2813 getExternalUriString("setup", id)); 2814 } 2815 } 2816 } 2817 if (projectionColumns.contains(UIProvider.AccountColumns.TYPE)) { 2818 final String type; 2819 if (info == null) { 2820 info = EmailServiceUtils.getServiceInfoForAccount(context, accountId); 2821 } 2822 if (info != null) { 2823 type = info.accountType; 2824 } else { 2825 type = "unknown"; 2826 } 2827 2828 values.put(UIProvider.AccountColumns.TYPE, type); 2829 } 2830 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX) && 2831 inboxMailboxId != Mailbox.NO_MAILBOX) { 2832 values.put(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX, 2833 uiUriString("uifolder", inboxMailboxId)); 2834 } 2835 if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_AUTHORITY)) { 2836 values.put(UIProvider.AccountColumns.SYNC_AUTHORITY, EmailContent.AUTHORITY); 2837 } 2838 if (projectionColumns.contains(UIProvider.AccountColumns.QUICK_RESPONSE_URI)) { 2839 values.put(UIProvider.AccountColumns.QUICK_RESPONSE_URI, 2840 combinedUriString("quickresponse/account", id)); 2841 } 2842 2843 final StringBuilder sb = genSelect(getAccountListMap(getContext()), uiProjection, values); 2844 sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?"); 2845 return sb.toString(); 2846 } 2847 2848 private static int autoAdvanceToUiValue(int autoAdvance) { 2849 switch(autoAdvance) { 2850 case Preferences.AUTO_ADVANCE_OLDER: 2851 return UIProvider.AutoAdvance.OLDER; 2852 case Preferences.AUTO_ADVANCE_NEWER: 2853 return UIProvider.AutoAdvance.NEWER; 2854 case Preferences.AUTO_ADVANCE_MESSAGE_LIST: 2855 default: 2856 return UIProvider.AutoAdvance.LIST; 2857 } 2858 } 2859 2860 private static int textZoomToUiValue(int textZoom) { 2861 switch(textZoom) { 2862 case Preferences.TEXT_ZOOM_HUGE: 2863 return UIProvider.MessageTextSize.HUGE; 2864 case Preferences.TEXT_ZOOM_LARGE: 2865 return UIProvider.MessageTextSize.LARGE; 2866 case Preferences.TEXT_ZOOM_NORMAL: 2867 return UIProvider.MessageTextSize.NORMAL; 2868 case Preferences.TEXT_ZOOM_SMALL: 2869 return UIProvider.MessageTextSize.SMALL; 2870 case Preferences.TEXT_ZOOM_TINY: 2871 return UIProvider.MessageTextSize.TINY; 2872 default: 2873 return UIProvider.MessageTextSize.NORMAL; 2874 } 2875 } 2876 2877 /** 2878 * Generate a Uri string for a combined mailbox uri 2879 * @param type the uri command type (e.g. "uimessages") 2880 * @param id the id of the item (e.g. an account, mailbox, or message id) 2881 * @return a Uri string 2882 */ 2883 private static String combinedUriString(String type, String id) { 2884 return "content://" + EmailContent.AUTHORITY + "/" + type + "/" + id; 2885 } 2886 2887 public static final long COMBINED_ACCOUNT_ID = 0x10000000; 2888 2889 /** 2890 * Generate an id for a combined mailbox of a given type 2891 * @param type the mailbox type for the combined mailbox 2892 * @return the id, as a String 2893 */ 2894 private static String combinedMailboxId(int type) { 2895 return Long.toString(Account.ACCOUNT_ID_COMBINED_VIEW + type); 2896 } 2897 2898 public static long getVirtualMailboxId(long accountId, int type) { 2899 return (accountId << 32) + type; 2900 } 2901 2902 private static boolean isVirtualMailbox(long mailboxId) { 2903 return mailboxId >= 0x100000000L; 2904 } 2905 2906 private static boolean isCombinedMailbox(long mailboxId) { 2907 return (mailboxId >> 32) == COMBINED_ACCOUNT_ID; 2908 } 2909 2910 private static long getVirtualMailboxAccountId(long mailboxId) { 2911 return mailboxId >> 32; 2912 } 2913 2914 private static String getVirtualMailboxAccountIdString(long mailboxId) { 2915 return Long.toString(mailboxId >> 32); 2916 } 2917 2918 private static int getVirtualMailboxType(long mailboxId) { 2919 return (int)(mailboxId & 0xF); 2920 } 2921 2922 private void addCombinedAccountRow(MatrixCursor mc) { 2923 final long lastUsedAccountId = 2924 Preferences.getPreferences(getContext()).getLastUsedAccountId(); 2925 final long id = Account.getDefaultAccountId(getContext(), lastUsedAccountId); 2926 if (id == Account.NO_ACCOUNT) return; 2927 2928 // Build a map of the requested columns to the appropriate positions 2929 final ImmutableMap.Builder<String, Integer> builder = 2930 new ImmutableMap.Builder<String, Integer>(); 2931 final String[] columnNames = mc.getColumnNames(); 2932 for (int i = 0; i < columnNames.length; i++) { 2933 builder.put(columnNames[i], i); 2934 } 2935 final Map<String, Integer> colPosMap = builder.build(); 2936 2937 final MailPrefs mailPrefs = MailPrefs.get(getContext()); 2938 2939 final Object[] values = new Object[columnNames.length]; 2940 if (colPosMap.containsKey(BaseColumns._ID)) { 2941 values[colPosMap.get(BaseColumns._ID)] = 0; 2942 } 2943 if (colPosMap.containsKey(UIProvider.AccountColumns.CAPABILITIES)) { 2944 values[colPosMap.get(UIProvider.AccountColumns.CAPABILITIES)] = 2945 AccountCapabilities.UNDO | AccountCapabilities.SENDING_UNAVAILABLE; 2946 } 2947 if (colPosMap.containsKey(UIProvider.AccountColumns.FOLDER_LIST_URI)) { 2948 values[colPosMap.get(UIProvider.AccountColumns.FOLDER_LIST_URI)] = 2949 combinedUriString("uifolders", COMBINED_ACCOUNT_ID_STRING); 2950 } 2951 if (colPosMap.containsKey(UIProvider.AccountColumns.NAME)) { 2952 values[colPosMap.get(UIProvider.AccountColumns.NAME)] = getContext().getString( 2953 R.string.mailbox_list_account_selector_combined_view); 2954 } 2955 if (colPosMap.containsKey(UIProvider.AccountColumns.TYPE)) { 2956 values[colPosMap.get(UIProvider.AccountColumns.TYPE)] = "unknown"; 2957 } 2958 if (colPosMap.containsKey(UIProvider.AccountColumns.UNDO_URI)) { 2959 values[colPosMap.get(UIProvider.AccountColumns.UNDO_URI)] = 2960 "'content://" + EmailContent.AUTHORITY + "/uiundo'"; 2961 } 2962 if (colPosMap.containsKey(UIProvider.AccountColumns.URI)) { 2963 values[colPosMap.get(UIProvider.AccountColumns.URI)] = 2964 combinedUriString("uiaccount", COMBINED_ACCOUNT_ID_STRING); 2965 } 2966 if (colPosMap.containsKey(UIProvider.AccountColumns.MIME_TYPE)) { 2967 values[colPosMap.get(UIProvider.AccountColumns.MIME_TYPE)] = 2968 EMAIL_APP_MIME_TYPE; 2969 } 2970 if (colPosMap.containsKey(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) { 2971 values[colPosMap.get(UIProvider.AccountColumns.SETTINGS_INTENT_URI)] = 2972 getExternalUriString("settings", COMBINED_ACCOUNT_ID_STRING); 2973 } 2974 if (colPosMap.containsKey(UIProvider.AccountColumns.COMPOSE_URI)) { 2975 values[colPosMap.get(UIProvider.AccountColumns.COMPOSE_URI)] = 2976 getExternalUriStringEmail2("compose", Long.toString(id)); 2977 } 2978 2979 // TODO: Get these from default account? 2980 Preferences prefs = Preferences.getPreferences(getContext()); 2981 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) { 2982 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)] = 2983 Integer.toString(UIProvider.AutoAdvance.NEWER); 2984 } 2985 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)) { 2986 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)] = 2987 Integer.toString(UIProvider.MessageTextSize.NORMAL); 2988 } 2989 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)) { 2990 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)] = 2991 Integer.toString(UIProvider.SnapHeaderValue.ALWAYS); 2992 } 2993 //.add(UIProvider.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE) 2994 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) { 2995 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)] = 2996 Integer.toString(mailPrefs.getDefaultReplyAll() 2997 ? UIProvider.DefaultReplyBehavior.REPLY_ALL 2998 : UIProvider.DefaultReplyBehavior.REPLY); 2999 } 3000 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) { 3001 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)] = 3002 getConversationListIcon(mailPrefs); 3003 } 3004 if (colPosMap.containsKey( 3005 UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS)) { 3006 values[colPosMap.get( 3007 UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS)] = 0; 3008 } 3009 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) { 3010 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)] = 3011 prefs.getConfirmDelete() ? 1 : 0; 3012 } 3013 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)) { 3014 values[colPosMap.get( 3015 UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)] = 0; 3016 } 3017 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) { 3018 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)] = 3019 prefs.getConfirmSend() ? 1 : 0; 3020 } 3021 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)) { 3022 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)] = 3023 combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX)); 3024 } 3025 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)) { 3026 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)] = 3027 combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX)); 3028 } 3029 3030 mc.addRow(values); 3031 } 3032 3033 private static int getConversationListIcon(MailPrefs mailPrefs) { 3034 return mailPrefs.getShowSenderImages() ? 3035 UIProvider.ConversationListIcon.SENDER_IMAGE : 3036 UIProvider.ConversationListIcon.NONE; 3037 } 3038 3039 private Cursor getVirtualMailboxCursor(long mailboxId) { 3040 MatrixCursor mc = new MatrixCursorWithCachedColumns(UIProvider.FOLDERS_PROJECTION, 1); 3041 mc.addRow(getVirtualMailboxRow(getVirtualMailboxAccountId(mailboxId), 3042 getVirtualMailboxType(mailboxId))); 3043 return mc; 3044 } 3045 3046 private Object[] getVirtualMailboxRow(long accountId, int mailboxType) { 3047 final long id = getVirtualMailboxId(accountId, mailboxType); 3048 final String idString = Long.toString(id); 3049 Object[] values = new Object[UIProvider.FOLDERS_PROJECTION.length]; 3050 values[UIProvider.FOLDER_ID_COLUMN] = id; 3051 values[UIProvider.FOLDER_URI_COLUMN] = combinedUriString("uifolder", idString); 3052 values[UIProvider.FOLDER_NAME_COLUMN] = getFolderDisplayName( 3053 getFolderTypeFromMailboxType(mailboxType), ""); 3054 // default empty string since all of these should use resource strings 3055 values[UIProvider.FOLDER_HAS_CHILDREN_COLUMN] = 0; 3056 values[UIProvider.FOLDER_CAPABILITIES_COLUMN] = 3057 UIProvider.FolderCapabilities.DELETE | UIProvider.FolderCapabilities.IS_VIRTUAL; 3058 values[UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN] = combinedUriString("uimessages", 3059 idString); 3060 3061 // Do any special handling 3062 final String accountIdString = Long.toString(accountId); 3063 switch (mailboxType) { 3064 case Mailbox.TYPE_INBOX: 3065 if (accountId == COMBINED_ACCOUNT_ID) { 3066 // Add the unread count 3067 final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI, 3068 MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID 3069 + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE 3070 + "=" + Mailbox.TYPE_INBOX + ") AND " + MessageColumns.FLAG_READ + "=0", 3071 null); 3072 values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = unreadCount; 3073 } 3074 // Add the icon 3075 values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_inbox; 3076 break; 3077 case Mailbox.TYPE_UNREAD: { 3078 // Add the unread count 3079 final String accountKeyClause; 3080 final String[] whereArgs; 3081 if (accountId == COMBINED_ACCOUNT_ID) { 3082 accountKeyClause = ""; 3083 whereArgs = null; 3084 } else { 3085 accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND "; 3086 whereArgs = new String[] { accountIdString }; 3087 } 3088 final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI, 3089 accountKeyClause + MessageColumns.FLAG_READ + "=0 AND " 3090 + MessageColumns.MAILBOX_KEY + " NOT IN (SELECT " + MailboxColumns.ID 3091 + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + "=" 3092 + Mailbox.TYPE_TRASH + ")", whereArgs); 3093 values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = unreadCount; 3094 // Add the icon 3095 values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_unread; 3096 break; 3097 } case Mailbox.TYPE_STARRED: { 3098 // Add the starred count as the unread count 3099 final String accountKeyClause; 3100 final String[] whereArgs; 3101 if (accountId == COMBINED_ACCOUNT_ID) { 3102 accountKeyClause = ""; 3103 whereArgs = null; 3104 } else { 3105 accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND "; 3106 whereArgs = new String[] { accountIdString }; 3107 } 3108 final int starredCount = EmailContent.count(getContext(), Message.CONTENT_URI, 3109 accountKeyClause + MessageColumns.FLAG_FAVORITE + "=1", whereArgs); 3110 values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = starredCount; 3111 // Add the icon 3112 values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_star; 3113 break; 3114 } 3115 } 3116 3117 return values; 3118 } 3119 3120 private Cursor uiAccounts(String[] uiProjection) { 3121 final Context context = getContext(); 3122 final SQLiteDatabase db = getDatabase(context); 3123 final Cursor accountIdCursor = 3124 db.rawQuery("select _id from " + Account.TABLE_NAME, new String[0]); 3125 final MatrixCursor mc; 3126 try { 3127 boolean combinedAccount = false; 3128 if (accountIdCursor.getCount() > 1) { 3129 combinedAccount = true; 3130 } 3131 final Bundle extras = new Bundle(); 3132 // Email always returns the accurate number of accounts 3133 extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, 1); 3134 mc = new MatrixCursorWithExtra(uiProjection, accountIdCursor.getCount(), extras); 3135 final Object[] values = new Object[uiProjection.length]; 3136 while (accountIdCursor.moveToNext()) { 3137 final String id = accountIdCursor.getString(0); 3138 final Cursor accountCursor = 3139 db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); 3140 try { 3141 if (accountCursor.moveToNext()) { 3142 for (int i = 0; i < uiProjection.length; i++) { 3143 values[i] = accountCursor.getString(i); 3144 } 3145 mc.addRow(values); 3146 } 3147 } finally { 3148 accountCursor.close(); 3149 } 3150 } 3151 if (combinedAccount) { 3152 addCombinedAccountRow(mc); 3153 } 3154 } finally { 3155 accountIdCursor.close(); 3156 } 3157 mc.setNotificationUri(context.getContentResolver(), UIPROVIDER_ALL_ACCOUNTS_NOTIFIER); 3158 3159 return mc; 3160 } 3161 3162 private Cursor uiQuickResponseAccount(String[] uiProjection, String account) { 3163 final Context context = getContext(); 3164 final SQLiteDatabase db = getDatabase(context); 3165 final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection); 3166 sb.append(" FROM " + QuickResponse.TABLE_NAME); 3167 sb.append(" WHERE " + QuickResponse.ACCOUNT_KEY + "=?"); 3168 final String query = sb.toString(); 3169 return db.rawQuery(query, new String[] {account}); 3170 } 3171 3172 private Cursor uiQuickResponseId(String[] uiProjection, String id) { 3173 final Context context = getContext(); 3174 final SQLiteDatabase db = getDatabase(context); 3175 final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection); 3176 sb.append(" FROM " + QuickResponse.TABLE_NAME); 3177 sb.append(" WHERE " + QuickResponse.ID + "=?"); 3178 final String query = sb.toString(); 3179 return db.rawQuery(query, new String[] {id}); 3180 } 3181 3182 private Cursor uiQuickResponse(String[] uiProjection) { 3183 final Context context = getContext(); 3184 final SQLiteDatabase db = getDatabase(context); 3185 final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection); 3186 sb.append(" FROM " + QuickResponse.TABLE_NAME); 3187 final String query = sb.toString(); 3188 return db.rawQuery(query, new String[0]); 3189 } 3190 3191 /** 3192 * Generate the "attachment list" SQLite query, given a projection from UnifiedEmail 3193 * 3194 * @param uiProjection as passed from UnifiedEmail 3195 * @param contentTypeQueryParameters list of mimeTypes, used as a filter for the attachments 3196 * or null if there are no query parameters 3197 * @return the SQLite query to be executed on the EmailProvider database 3198 */ 3199 private static String genQueryAttachments(String[] uiProjection, 3200 List<String> contentTypeQueryParameters) { 3201 // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENT 3202 ContentValues values = new ContentValues(1); 3203 values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1); 3204 StringBuilder sb = genSelect(getAttachmentMap(), uiProjection, values); 3205 sb.append(" FROM ") 3206 .append(Attachment.TABLE_NAME) 3207 .append(" WHERE ") 3208 .append(AttachmentColumns.MESSAGE_KEY) 3209 .append(" =? "); 3210 3211 // Filter for certain content types. 3212 // The filter works by adding LIKE operators for each 3213 // content type you wish to request. Content types 3214 // are filtered by performing a case-insensitive "starts with" 3215 // filter. IE, "image/" would return "image/png" as well as "image/jpeg". 3216 if (contentTypeQueryParameters != null && !contentTypeQueryParameters.isEmpty()) { 3217 final int size = contentTypeQueryParameters.size(); 3218 sb.append("AND ("); 3219 for (int i = 0; i < size; i++) { 3220 final String contentType = contentTypeQueryParameters.get(i); 3221 sb.append(AttachmentColumns.MIME_TYPE) 3222 .append(" LIKE '") 3223 .append(contentType) 3224 .append("%'"); 3225 3226 if (i != size - 1) { 3227 sb.append(" OR "); 3228 } 3229 } 3230 sb.append(")"); 3231 } 3232 return sb.toString(); 3233 } 3234 3235 /** 3236 * Generate the "single attachment" SQLite query, given a projection from UnifiedEmail 3237 * 3238 * @param uiProjection as passed from UnifiedEmail 3239 * @return the SQLite query to be executed on the EmailProvider database 3240 */ 3241 private String genQueryAttachment(String[] uiProjection, String idString) { 3242 Long id = Long.parseLong(idString); 3243 Attachment att = Attachment.restoreAttachmentWithId(getContext(), id); 3244 // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENTS 3245 ContentValues values = new ContentValues(2); 3246 values.put(AttachmentColumns.CONTENT_URI, 3247 AttachmentUtilities.getAttachmentUri(att.mAccountKey, id).toString()); 3248 values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1); 3249 StringBuilder sb = genSelect(getAttachmentMap(), uiProjection, values); 3250 sb.append(" FROM ") 3251 .append(Attachment.TABLE_NAME) 3252 .append(" WHERE ") 3253 .append(AttachmentColumns.ID) 3254 .append(" =? "); 3255 return sb.toString(); 3256 } 3257 3258 /** 3259 * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail 3260 * 3261 * @param uiProjection as passed from UnifiedEmail 3262 * @return the SQLite query to be executed on the EmailProvider database 3263 */ 3264 private static String genQuerySubfolders(String[] uiProjection) { 3265 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 3266 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY + 3267 " =? ORDER BY "); 3268 sb.append(MAILBOX_ORDER_BY); 3269 return sb.toString(); 3270 } 3271 3272 private static final String COMBINED_ACCOUNT_ID_STRING = Long.toString(COMBINED_ACCOUNT_ID); 3273 3274 /** 3275 * Returns a cursor over all the folders for a specific URI which corresponds to a single 3276 * account. 3277 * @param uri uri to query 3278 * @param uiProjection projection 3279 * @return query result cursor 3280 */ 3281 private Cursor uiFolders(Uri uri, String[] uiProjection) { 3282 Context context = getContext(); 3283 SQLiteDatabase db = getDatabase(context); 3284 String id = uri.getPathSegments().get(1); 3285 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 3286 MatrixCursor mc = new MatrixCursorWithCachedColumns(UIProvider.FOLDERS_PROJECTION, 3); 3287 Object[] row; 3288 row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX); 3289 mc.addRow(row); 3290 row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_STARRED); 3291 mc.addRow(row); 3292 row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_UNREAD); 3293 mc.addRow(row); 3294 3295 final Uri notifyUri = 3296 UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build(); 3297 mc.setNotificationUri(context.getContentResolver(), notifyUri); 3298 return mc; 3299 } else { 3300 Cursor c = db.rawQuery(genQueryAccountMailboxes(uiProjection), new String[] {id}); 3301 c = getFolderListCursor(db, c, uiProjection); 3302 // Add starred virtual folder to the cursor 3303 // Show number of messages as unread count (for backward compatibility) 3304 MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection, 2); 3305 final long acctId = Long.parseLong(id); 3306 Object[] row = getVirtualMailboxRow(acctId, Mailbox.TYPE_STARRED); 3307 mc.addRow(row); 3308 row = getVirtualMailboxRow(acctId, Mailbox.TYPE_UNREAD); 3309 mc.addRow(row); 3310 3311 final Uri notifyUri = 3312 UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build(); 3313 mc.setNotificationUri(context.getContentResolver(), notifyUri); 3314 c.setNotificationUri(context.getContentResolver(), notifyUri); 3315 Cursor[] cursors = new Cursor[] {mc, c}; 3316 return new MergeCursor(cursors); 3317 } 3318 } 3319 3320 /** 3321 * Returns an array of the default recent folders for a given URI which is unique for an 3322 * account. Some accounts might not have default recent folders, in which case an empty array 3323 * is returned. 3324 * @param id account id 3325 * @return array of URIs 3326 */ 3327 private Uri[] defaultRecentFolders(final String id) { 3328 final SQLiteDatabase db = getDatabase(getContext()); 3329 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 3330 // We don't have default recents for the combined view. 3331 return new Uri[0]; 3332 } 3333 // We search for the types we want, and find corresponding IDs. 3334 final String[] idAndType = { BaseColumns._ID, UIProvider.FolderColumns.TYPE }; 3335 3336 // Sent, Drafts, and Starred are the default recents. 3337 final StringBuilder sb = genSelect(getFolderListMap(), idAndType); 3338 sb.append(" FROM ") 3339 .append(Mailbox.TABLE_NAME) 3340 .append(" WHERE ") 3341 .append(MailboxColumns.ACCOUNT_KEY) 3342 .append(" = ") 3343 .append(id) 3344 .append(" AND ") 3345 .append(MailboxColumns.TYPE) 3346 .append(" IN (") 3347 .append(Mailbox.TYPE_SENT) 3348 .append(", ") 3349 .append(Mailbox.TYPE_DRAFTS) 3350 .append(", ") 3351 .append(Mailbox.TYPE_STARRED) 3352 .append(")"); 3353 LogUtils.d(TAG, "defaultRecentFolders: Query is %s", sb); 3354 final Cursor c = db.rawQuery(sb.toString(), null); 3355 if (c == null || c.getCount() <= 0 || !c.moveToFirst()) { 3356 return new Uri[0]; 3357 } 3358 // Read all the IDs of the mailboxes, and turn them into URIs. 3359 final Uri[] recentFolders = new Uri[c.getCount()]; 3360 int i = 0; 3361 do { 3362 final long folderId = c.getLong(0); 3363 recentFolders[i] = uiUri("uifolder", folderId); 3364 LogUtils.d(TAG, "Default recent folder: %d, with uri %s", folderId, recentFolders[i]); 3365 ++i; 3366 } while (c.moveToNext()); 3367 return recentFolders; 3368 } 3369 3370 /** 3371 * Wrapper that handles the visibility feature (i.e. the conversation list is visible, so 3372 * any pending notifications for the corresponding mailbox should be canceled). We also handle 3373 * getExtras() to provide a snapshot of the mailbox's status 3374 */ 3375 static class EmailConversationCursor extends CursorWrapper { 3376 private final long mMailboxId; 3377 private final Context mContext; 3378 private final FolderList mFolderList; 3379 private final Bundle mExtras = new Bundle(); 3380 3381 /** 3382 * When showing a folder, if it's been at least this long since the last sync, 3383 * force a folder refresh. 3384 */ 3385 private static final long AUTO_REFRESH_INTERVAL_MS = 5 * DateUtils.MINUTE_IN_MILLIS; 3386 3387 public EmailConversationCursor(final Context context, final Cursor cursor, 3388 final Folder folder, final long mailboxId) { 3389 super(cursor); 3390 mMailboxId = mailboxId; 3391 mContext = context; 3392 mFolderList = FolderList.copyOf(Lists.newArrayList(folder)); 3393 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 3394 3395 if (mailbox != null) { 3396 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_ERROR, 3397 mailbox.mUiLastSyncResult); 3398 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT, mailbox.mTotalCount); 3399 if (mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_BACKGROUND 3400 || mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_USER) { 3401 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, 3402 UIProvider.CursorStatus.LOADING); 3403 } else if (mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_NONE) { 3404 if (mailbox.mSyncInterval == 0 && 3405 System.currentTimeMillis() - mailbox.mSyncTime 3406 > AUTO_REFRESH_INTERVAL_MS) { 3407 // This will be syncing momentarily 3408 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, 3409 UIProvider.CursorStatus.LOADING); 3410 } else { 3411 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, 3412 UIProvider.CursorStatus.COMPLETE); 3413 } 3414 } else { 3415 LogUtils.d(Logging.LOG_TAG, 3416 "Unknown mailbox sync status" + mailbox.mUiSyncStatus); 3417 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, 3418 UIProvider.CursorStatus.COMPLETE); 3419 } 3420 } else { 3421 // TODO for virtual mailboxes, we may want to do something besides just fake it 3422 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_ERROR, 3423 UIProvider.LastSyncResult.SUCCESS); 3424 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT, 3425 cursor != null ? cursor.getCount() : 0); 3426 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, 3427 UIProvider.CursorStatus.COMPLETE); 3428 } 3429 } 3430 3431 @Override 3432 public Bundle getExtras() { 3433 return mExtras; 3434 } 3435 3436 @Override 3437 public Bundle respond(Bundle params) { 3438 final String setVisibilityKey = 3439 UIProvider.ConversationCursorCommand.COMMAND_KEY_SET_VISIBILITY; 3440 if (params.containsKey(setVisibilityKey)) { 3441 final boolean visible = params.getBoolean(setVisibilityKey); 3442 if (visible) { 3443 // Mark all messages as seen 3444 final ContentResolver resolver = mContext.getContentResolver(); 3445 final ContentValues contentValues = new ContentValues(1); 3446 contentValues.put(MessageColumns.FLAG_SEEN, true); 3447 final Uri uri = EmailContent.Message.CONTENT_URI; 3448 resolver.update(uri, contentValues, MessageColumns.MAILBOX_KEY + " = ?", 3449 new String[] {String.valueOf(mMailboxId)}); 3450 if (params.containsKey( 3451 UIProvider.ConversationCursorCommand.COMMAND_KEY_ENTERED_FOLDER)) { 3452 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId); 3453 if (mailbox != null) { 3454 // For non-push mailboxes, if it's stale (i.e. last sync was a while 3455 // ago), force a sync. 3456 // TODO: Fix the check for whether we're non-push? Right now it checks 3457 // whether we are participating in account sync rules. 3458 if (mailbox.mSyncInterval == 0) { 3459 final long timeSinceLastSync = 3460 System.currentTimeMillis() - mailbox.mSyncTime; 3461 if (timeSinceLastSync > AUTO_REFRESH_INTERVAL_MS) { 3462 final Uri refreshUri = Uri.parse(EmailContent.CONTENT_URI + 3463 "/" + QUERY_UIREFRESH + "/" + mailbox.mId); 3464 resolver.query(refreshUri, null, null, null, null); 3465 } 3466 } 3467 } 3468 } 3469 } 3470 } 3471 // Return success 3472 final Bundle response = new Bundle(2); 3473 3474 response.putString(setVisibilityKey, 3475 UIProvider.ConversationCursorCommand.COMMAND_RESPONSE_OK); 3476 3477 final String rawFoldersKey = 3478 UIProvider.ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS; 3479 if (params.containsKey(rawFoldersKey)) { 3480 response.putParcelable(rawFoldersKey, mFolderList); 3481 } 3482 3483 final String convInfoKey = 3484 UIProvider.ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO; 3485 if (params.containsKey(convInfoKey)) { 3486 response.putParcelable(convInfoKey, generateConversationInfo()); 3487 } 3488 3489 return response; 3490 } 3491 3492 private ConversationInfo generateConversationInfo() { 3493 // TODO This populates a very minimal ConversationInfo because it is currently only 3494 // needed by NestedFolderTeaserView 3495 final int numMessages = getInt(getColumnIndex(ConversationColumns.NUM_MESSAGES)); 3496 final ConversationInfo conversationInfo = new ConversationInfo(numMessages); 3497 3498 final boolean isRead = getInt(getColumnIndex(ConversationColumns.READ)) != 0; 3499 final boolean isStarred = getInt(getColumnIndex(ConversationColumns.STARRED)) != 0; 3500 final String senderString = getString(getColumnIndex(MessageColumns.DISPLAY_NAME)); 3501 // TODO We probably want to parse out the email from this 3502 final String email = getString(getColumnIndex(MessageColumns.FROM_LIST)); 3503 3504 final MessageInfo messageInfo = new MessageInfo(isRead, isStarred, senderString, 3505 0 /* priority */, email); 3506 conversationInfo.addMessage(messageInfo); 3507 3508 return conversationInfo; 3509 } 3510 } 3511 3512 /** 3513 * Convenience method to create a {@link Folder} 3514 * @param context to get a {@link ContentResolver} 3515 * @param mailboxId id of the {@link Mailbox} that we want 3516 * @return the {@link Folder} or null 3517 */ 3518 public static Folder getFolder(Context context, long mailboxId) { 3519 final ContentResolver resolver = context.getContentResolver(); 3520 final Cursor fc = resolver.query(EmailProvider.uiUri("uifolder", mailboxId), 3521 UIProvider.FOLDERS_PROJECTION, null, null, null); 3522 3523 if (fc == null) { 3524 LogUtils.e(TAG, "Null folder cursor for mailboxId %d", mailboxId); 3525 return null; 3526 } 3527 3528 Folder uiFolder = null; 3529 try { 3530 if (fc.moveToFirst()) { 3531 uiFolder = new Folder(fc); 3532 } 3533 } finally { 3534 fc.close(); 3535 } 3536 return uiFolder; 3537 } 3538 3539 static class AttachmentsCursor extends CursorWrapper { 3540 private final int mContentUriIndex; 3541 private final int mUriIndex; 3542 private final Context mContext; 3543 3544 public AttachmentsCursor(Context context, Cursor cursor) { 3545 super(cursor); 3546 mContentUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.CONTENT_URI); 3547 mUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.URI); 3548 mContext = context; 3549 } 3550 3551 @Override 3552 public String getString(int column) { 3553 if (column == mContentUriIndex) { 3554 final Uri uri = Uri.parse(getString(mUriIndex)); 3555 final long id = Long.parseLong(uri.getLastPathSegment()); 3556 final Attachment att = Attachment.restoreAttachmentWithId(mContext, id); 3557 if (att == null) return ""; 3558 3559 final String contentUri; 3560 // Until the package installer can handle opening apks from a content:// uri, for 3561 // any apk that was successfully saved in external storage, return the 3562 // content uri from the attachment 3563 if (att.mUiDestination == UIProvider.AttachmentDestination.EXTERNAL && 3564 att.mUiState == UIProvider.AttachmentState.SAVED && 3565 TextUtils.equals(att.mMimeType, MimeType.ANDROID_ARCHIVE)) { 3566 contentUri = att.getContentUri(); 3567 } else { 3568 contentUri = 3569 AttachmentUtilities.getAttachmentUri(att.mAccountKey, id).toString(); 3570 } 3571 return contentUri; 3572 } else { 3573 return super.getString(column); 3574 } 3575 } 3576 } 3577 3578 /** 3579 * For debugging purposes; shouldn't be used in production code 3580 */ 3581 @SuppressWarnings("unused") 3582 static class CloseDetectingCursor extends CursorWrapper { 3583 3584 public CloseDetectingCursor(Cursor cursor) { 3585 super(cursor); 3586 } 3587 3588 @Override 3589 public void close() { 3590 super.close(); 3591 LogUtils.d(TAG, "Closing cursor", new Error()); 3592 } 3593 } 3594 3595 /** 3596 * We need to do individual queries for the mailboxes in order to get correct 3597 * folder capabilities. 3598 */ 3599 private Cursor getFolderListCursor(SQLiteDatabase db, Cursor c, String[] uiProjection) { 3600 final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection); 3601 final String[] args = new String[1]; 3602 final int projectionLength = uiProjection.length; 3603 final List<String> projectionList = Arrays.asList(uiProjection); 3604 final int nameColumn = projectionList.indexOf(UIProvider.FolderColumns.NAME); 3605 final int typeColumn = projectionList.indexOf(UIProvider.FolderColumns.TYPE); 3606 3607 try { 3608 // Loop through mailboxes, building matrix cursor 3609 while (c.moveToNext()) { 3610 final String id = c.getString(0); 3611 args[0] = id; 3612 final Cursor mailboxCursor = db.rawQuery(genQueryMailbox(uiProjection, id), args); 3613 if (mailboxCursor.moveToNext()) { 3614 getUiFolderCursorRowFromMailboxCursorRow( 3615 mc, projectionLength, mailboxCursor, nameColumn, typeColumn); 3616 } 3617 } 3618 } finally { 3619 c.close(); 3620 } 3621 return mc; 3622 } 3623 3624 /** 3625 * Converts a mailbox in a row of the mailboxCursor into a row 3626 * in the supplied {@link MatrixCursor} in the format required for {@link Folder}. 3627 * As a convenience, the modified {@link MatrixCursor} is also returned. 3628 * @param mc the {@link MatrixCursor} into which the mailbox data will be converted 3629 * @param projectionLength the length of the projection for this Cursor 3630 * @param mailboxCursor the cursor supplying the mailbox data 3631 * @param nameColumn column in the cursor containing the folder name value 3632 * @param typeColumn column in the cursor containing the folder type value 3633 * @return the {@link MatrixCursor} containing the transformed data. 3634 */ 3635 private Cursor getUiFolderCursorRowFromMailboxCursorRow( 3636 MatrixCursor mc, int projectionLength, Cursor mailboxCursor, 3637 int nameColumn, int typeColumn) { 3638 final MatrixCursor.RowBuilder builder = mc.newRow(); 3639 for (int i = 0; i < projectionLength; i++) { 3640 // If we are at the name column, get the type 3641 // and use it to use a properly translated string 3642 // from resources instead of the display name. 3643 // This ignores display names for system mailboxes. 3644 if (nameColumn == i) { 3645 // We implicitly assume that if name is requested, 3646 // type has also been requested. If not, this will 3647 // error in unknown ways. 3648 final int type = mailboxCursor.getInt(typeColumn); 3649 builder.add(getFolderDisplayName(type, mailboxCursor.getString(i))); 3650 } else { 3651 builder.add(mailboxCursor.getString(i)); 3652 } 3653 } 3654 return mc; 3655 } 3656 3657 /** 3658 * Returns a {@link String} from Resources corresponding 3659 * to the {@link UIProvider.FolderType} requested. 3660 * @param folderType {@link UIProvider.FolderType} value for the folder 3661 * @param defaultName a {@link String} to use in case the {@link UIProvider.FolderType} 3662 * provided is not a system folder. 3663 * @return a {@link String} to use as the display name for the folder 3664 */ 3665 private String getFolderDisplayName(int folderType, String defaultName) { 3666 final int resId; 3667 switch (folderType) { 3668 case UIProvider.FolderType.INBOX: 3669 resId = R.string.mailbox_name_display_inbox; 3670 break; 3671 case UIProvider.FolderType.OUTBOX: 3672 resId = R.string.mailbox_name_display_outbox; 3673 break; 3674 case UIProvider.FolderType.DRAFT: 3675 resId = R.string.mailbox_name_display_drafts; 3676 break; 3677 case UIProvider.FolderType.TRASH: 3678 resId = R.string.mailbox_name_display_trash; 3679 break; 3680 case UIProvider.FolderType.SENT: 3681 resId = R.string.mailbox_name_display_sent; 3682 break; 3683 case UIProvider.FolderType.SPAM: 3684 resId = R.string.mailbox_name_display_junk; 3685 break; 3686 case UIProvider.FolderType.STARRED: 3687 resId = R.string.mailbox_name_display_starred; 3688 break; 3689 case UIProvider.FolderType.UNREAD: 3690 resId = R.string.mailbox_name_display_unread; 3691 break; 3692 default: 3693 return defaultName; 3694 } 3695 return getContext().getString(resId); 3696 } 3697 3698 /** 3699 * Converts a {@link Mailbox} type value to its {@link UIProvider.FolderType} 3700 * equivalent. 3701 * @param mailboxType a {@link Mailbox} type 3702 * @return a {@link UIProvider.FolderType} value 3703 */ 3704 private static int getFolderTypeFromMailboxType(int mailboxType) { 3705 switch (mailboxType) { 3706 case Mailbox.TYPE_INBOX: 3707 return UIProvider.FolderType.INBOX; 3708 case Mailbox.TYPE_OUTBOX: 3709 return UIProvider.FolderType.OUTBOX; 3710 case Mailbox.TYPE_DRAFTS: 3711 return UIProvider.FolderType.DRAFT; 3712 case Mailbox.TYPE_TRASH: 3713 return UIProvider.FolderType.TRASH; 3714 case Mailbox.TYPE_SENT: 3715 return UIProvider.FolderType.SENT; 3716 case Mailbox.TYPE_JUNK: 3717 return UIProvider.FolderType.SPAM; 3718 case Mailbox.TYPE_STARRED: 3719 return UIProvider.FolderType.STARRED; 3720 case Mailbox.TYPE_UNREAD: 3721 return UIProvider.FolderType.UNREAD; 3722 case Mailbox.TYPE_SEARCH: 3723 // TODO Can the DEFAULT type be removed from SEARCH folders? 3724 return UIProvider.FolderType.DEFAULT | UIProvider.FolderType.SEARCH; 3725 default: 3726 return UIProvider.FolderType.DEFAULT; 3727 } 3728 } 3729 3730 /** 3731 * Handle UnifiedEmail queries here (dispatched from query()) 3732 * 3733 * @param match the UriMatcher match for the original uri passed in from UnifiedEmail 3734 * @param uri the original uri passed in from UnifiedEmail 3735 * @param uiProjection the projection passed in from UnifiedEmail 3736 * @param unseenOnly <code>true</code> to only return unseen messages (where supported) 3737 * @return the result Cursor 3738 */ 3739 private Cursor uiQuery(int match, Uri uri, String[] uiProjection, final boolean unseenOnly) { 3740 Context context = getContext(); 3741 ContentResolver resolver = context.getContentResolver(); 3742 SQLiteDatabase db = getDatabase(context); 3743 // Should we ever return null, or throw an exception?? 3744 Cursor c = null; 3745 String id = uri.getPathSegments().get(1); 3746 Uri notifyUri = null; 3747 switch(match) { 3748 case UI_ALL_FOLDERS: 3749 // TODO: Should this just do uiFolders()? 3750 c = db.rawQuery(genQueryAccountAllMailboxes(uiProjection), new String[] {id}); 3751 c = getFolderListCursor(db, c, uiProjection); 3752 notifyUri = 3753 UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build(); 3754 break; 3755 case UI_RECENT_FOLDERS: 3756 c = db.rawQuery(genQueryRecentMailboxes(uiProjection), new String[] {id}); 3757 notifyUri = UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build(); 3758 break; 3759 case UI_SUBFOLDERS: 3760 c = db.rawQuery(genQuerySubfolders(uiProjection), new String[] {id}); 3761 c = getFolderListCursor(db, c, uiProjection); 3762 // Get notifications for any folder changes on this account. This is broader than 3763 // we need but otherwise we'd need for every folder change to notify on all relevant 3764 // subtrees. For now we opt for simplicity. 3765 final long accountId = Mailbox.getAccountIdForMailbox(context, id); 3766 notifyUri = ContentUris.withAppendedId(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId); 3767 break; 3768 case UI_MESSAGES: 3769 long mailboxId = Long.parseLong(id); 3770 final Folder folder = getFolder(context, mailboxId); 3771 if (folder == null) { 3772 // This mailboxId is bogus. Return an empty cursor 3773 // TODO: Make callers of this query handle null cursors instead b/10819309 3774 return new MatrixCursor(uiProjection); 3775 } 3776 if (isVirtualMailbox(mailboxId)) { 3777 c = getVirtualMailboxMessagesCursor(db, uiProjection, mailboxId, unseenOnly); 3778 } else { 3779 c = db.rawQuery( 3780 genQueryMailboxMessages(uiProjection, unseenOnly), new String[] {id}); 3781 } 3782 notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build(); 3783 c = new EmailConversationCursor(context, c, folder, mailboxId); 3784 break; 3785 case UI_MESSAGE: 3786 MessageQuery qq = genQueryViewMessage(uiProjection, id); 3787 String sql = qq.query; 3788 String attJson = qq.attachmentJson; 3789 // With attachments, we have another argument to bind 3790 if (attJson != null) { 3791 c = db.rawQuery(sql, new String[] {attJson, id}); 3792 } else { 3793 c = db.rawQuery(sql, new String[] {id}); 3794 } 3795 notifyUri = UIPROVIDER_MESSAGE_NOTIFIER.buildUpon().appendPath(id).build(); 3796 break; 3797 case UI_ATTACHMENTS: 3798 final List<String> contentTypeQueryParameters = 3799 uri.getQueryParameters(PhotoContract.ContentTypeParameters.CONTENT_TYPE); 3800 c = db.rawQuery(genQueryAttachments(uiProjection, contentTypeQueryParameters), 3801 new String[] {id}); 3802 c = new AttachmentsCursor(context, c); 3803 notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build(); 3804 break; 3805 case UI_ATTACHMENT: 3806 c = db.rawQuery(genQueryAttachment(uiProjection, id), new String[] {id}); 3807 notifyUri = UIPROVIDER_ATTACHMENT_NOTIFIER.buildUpon().appendPath(id).build(); 3808 break; 3809 case UI_FOLDER: 3810 mailboxId = Long.parseLong(id); 3811 if (isVirtualMailbox(mailboxId)) { 3812 c = getVirtualMailboxCursor(mailboxId); 3813 notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(id).build(); 3814 } else { 3815 c = db.rawQuery(genQueryMailbox(uiProjection, id), new String[]{id}); 3816 final List<String> projectionList = Arrays.asList(uiProjection); 3817 final int nameColumn = projectionList.indexOf(UIProvider.FolderColumns.NAME); 3818 final int typeColumn = projectionList.indexOf(UIProvider.FolderColumns.TYPE); 3819 if (c.moveToFirst()) { 3820 c = getUiFolderCursorRowFromMailboxCursorRow( 3821 new MatrixCursorWithCachedColumns(uiProjection), 3822 uiProjection.length, c, nameColumn, typeColumn); 3823 } 3824 notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(id).build(); 3825 } 3826 break; 3827 case UI_ACCOUNT: 3828 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 3829 MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection, 1); 3830 addCombinedAccountRow(mc); 3831 c = mc; 3832 } else { 3833 c = db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); 3834 } 3835 notifyUri = UIPROVIDER_ACCOUNT_NOTIFIER.buildUpon().appendPath(id).build(); 3836 break; 3837 case UI_CONVERSATION: 3838 c = db.rawQuery(genQueryConversation(uiProjection), new String[] {id}); 3839 break; 3840 } 3841 if (notifyUri != null) { 3842 c.setNotificationUri(resolver, notifyUri); 3843 } 3844 return c; 3845 } 3846 3847 /** 3848 * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need 3849 * a few of the fields 3850 * @param uiAtt the UIProvider attachment to convert 3851 * @param cachedFile the path to the cached file to 3852 * @return the EmailProvider attachment 3853 */ 3854 // TODO(pwestbro): once the Attachment contains the cached uri, the second parameter can be 3855 // removed 3856 private static Attachment convertUiAttachmentToAttachment( 3857 com.android.mail.providers.Attachment uiAtt, String cachedFile) { 3858 final Attachment att = new Attachment(); 3859 att.setContentUri(uiAtt.contentUri.toString()); 3860 3861 if (!TextUtils.isEmpty(cachedFile)) { 3862 // Generate the content provider uri for this cached file 3863 final Uri.Builder cachedFileBuilder = Uri.parse( 3864 "content://" + EmailContent.AUTHORITY + "/attachment/cachedFile").buildUpon(); 3865 cachedFileBuilder.appendQueryParameter(Attachment.CACHED_FILE_QUERY_PARAM, cachedFile); 3866 att.setCachedFileUri(cachedFileBuilder.build().toString()); 3867 } 3868 3869 att.mFileName = uiAtt.getName(); 3870 att.mMimeType = uiAtt.getContentType(); 3871 att.mSize = uiAtt.size; 3872 return att; 3873 } 3874 3875 /** 3876 * Create a mailbox given the account and mailboxType. 3877 */ 3878 private Mailbox createMailbox(long accountId, int mailboxType) { 3879 Context context = getContext(); 3880 Mailbox box = Mailbox.newSystemMailbox(context, accountId, mailboxType); 3881 // Make sure drafts and save will show up in recents... 3882 // If these already exist (from old Email app), they will have touch times 3883 switch (mailboxType) { 3884 case Mailbox.TYPE_DRAFTS: 3885 box.mLastTouchedTime = Mailbox.DRAFTS_DEFAULT_TOUCH_TIME; 3886 break; 3887 case Mailbox.TYPE_SENT: 3888 box.mLastTouchedTime = Mailbox.SENT_DEFAULT_TOUCH_TIME; 3889 break; 3890 } 3891 box.save(context); 3892 return box; 3893 } 3894 3895 /** 3896 * Given an account name and a mailbox type, return that mailbox, creating it if necessary 3897 * @param accountId the account id to use 3898 * @param mailboxType the type of mailbox we're trying to find 3899 * @return the mailbox of the given type for the account in the uri, or null if not found 3900 */ 3901 private Mailbox getMailboxByAccountIdAndType(final long accountId, final int mailboxType) { 3902 Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), accountId, mailboxType); 3903 if (mailbox == null) { 3904 mailbox = createMailbox(accountId, mailboxType); 3905 } 3906 return mailbox; 3907 } 3908 3909 /** 3910 * Given a mailbox and the content values for a message, create/save the message in the mailbox 3911 * @param mailbox the mailbox to use 3912 * @param extras the bundle containing the message fields 3913 * @return the uri of the newly created message 3914 * TODO(yph): The following fields are available in extras but unused, verify whether they 3915 * should be respected: 3916 * - UIProvider.MessageColumns.SNIPPET 3917 * - UIProvider.MessageColumns.REPLY_TO 3918 * - UIProvider.MessageColumns.FROM 3919 * - UIProvider.MessageColumns.CUSTOM_FROM_ADDRESS 3920 */ 3921 private Uri uiSaveMessage(Message msg, Mailbox mailbox, Bundle extras) { 3922 final Context context = getContext(); 3923 // Fill in the message 3924 final Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey); 3925 if (account == null) return null; 3926 msg.mFrom = account.mEmailAddress; 3927 msg.mTimeStamp = System.currentTimeMillis(); 3928 msg.mTo = extras.getString(UIProvider.MessageColumns.TO); 3929 msg.mCc = extras.getString(UIProvider.MessageColumns.CC); 3930 msg.mBcc = extras.getString(UIProvider.MessageColumns.BCC); 3931 msg.mSubject = extras.getString(UIProvider.MessageColumns.SUBJECT); 3932 msg.mText = extras.getString(UIProvider.MessageColumns.BODY_TEXT); 3933 msg.mHtml = extras.getString(UIProvider.MessageColumns.BODY_HTML); 3934 msg.mMailboxKey = mailbox.mId; 3935 msg.mAccountKey = mailbox.mAccountKey; 3936 msg.mDisplayName = msg.mTo; 3937 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 3938 msg.mFlagRead = true; 3939 msg.mFlagSeen = true; 3940 final Integer quoteStartPos = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS); 3941 msg.mQuotedTextStartPos = quoteStartPos == null ? 0 : quoteStartPos; 3942 int flags = 0; 3943 final int draftType = extras.getInt(UIProvider.MessageColumns.DRAFT_TYPE); 3944 switch(draftType) { 3945 case DraftType.FORWARD: 3946 flags |= Message.FLAG_TYPE_FORWARD; 3947 break; 3948 case DraftType.REPLY_ALL: 3949 flags |= Message.FLAG_TYPE_REPLY_ALL; 3950 //$FALL-THROUGH$ 3951 case DraftType.REPLY: 3952 flags |= Message.FLAG_TYPE_REPLY; 3953 break; 3954 case DraftType.COMPOSE: 3955 flags |= Message.FLAG_TYPE_ORIGINAL; 3956 break; 3957 } 3958 int draftInfo = 0; 3959 if (extras.containsKey(UIProvider.MessageColumns.QUOTE_START_POS)) { 3960 draftInfo = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS); 3961 if (extras.getInt(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT) != 0) { 3962 draftInfo |= Message.DRAFT_INFO_APPEND_REF_MESSAGE; 3963 } 3964 } 3965 if (!extras.containsKey(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT)) { 3966 flags |= Message.FLAG_NOT_INCLUDE_QUOTED_TEXT; 3967 } 3968 msg.mDraftInfo = draftInfo; 3969 msg.mFlags = flags; 3970 3971 final String ref = extras.getString(UIProvider.MessageColumns.REF_MESSAGE_ID); 3972 if (ref != null && msg.mQuotedTextStartPos >= 0) { 3973 String refId = Uri.parse(ref).getLastPathSegment(); 3974 try { 3975 msg.mSourceKey = Long.parseLong(refId); 3976 } catch (NumberFormatException e) { 3977 // This will be zero; the default 3978 } 3979 } 3980 3981 // Get attachments from the ContentValues 3982 final List<com.android.mail.providers.Attachment> uiAtts = 3983 com.android.mail.providers.Attachment.fromJSONArray( 3984 extras.getString(UIProvider.MessageColumns.ATTACHMENTS)); 3985 final ArrayList<Attachment> atts = new ArrayList<Attachment>(); 3986 boolean hasUnloadedAttachments = false; 3987 Bundle attachmentFds = 3988 extras.getParcelable(UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP); 3989 for (com.android.mail.providers.Attachment uiAtt: uiAtts) { 3990 final Uri attUri = uiAtt.uri; 3991 if (attUri != null && attUri.getAuthority().equals(EmailContent.AUTHORITY)) { 3992 // If it's one of ours, retrieve the attachment and add it to the list 3993 final long attId = Long.parseLong(attUri.getLastPathSegment()); 3994 final Attachment att = Attachment.restoreAttachmentWithId(context, attId); 3995 if (att != null) { 3996 // We must clone the attachment into a new one for this message; easiest to 3997 // use a parcel here 3998 final Parcel p = Parcel.obtain(); 3999 att.writeToParcel(p, 0); 4000 p.setDataPosition(0); 4001 final Attachment attClone = new Attachment(p); 4002 p.recycle(); 4003 // Clear the messageKey (this is going to be a new attachment) 4004 attClone.mMessageKey = 0; 4005 // If we're sending this, it's not loaded, and we're not smart forwarding 4006 // add the download flag, so that ADS will start up 4007 if (mailbox.mType == Mailbox.TYPE_OUTBOX && att.getContentUri() == null && 4008 ((account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) { 4009 attClone.mFlags |= Attachment.FLAG_DOWNLOAD_FORWARD; 4010 hasUnloadedAttachments = true; 4011 } 4012 atts.add(attClone); 4013 } 4014 } else { 4015 // Cache the attachment. This will allow us to send it, if the permissions are 4016 // revoked 4017 final String cachedFileUri = 4018 AttachmentUtils.cacheAttachmentUri(context, uiAtt, attachmentFds); 4019 4020 // Convert external attachment to one of ours and add to the list 4021 atts.add(convertUiAttachmentToAttachment(uiAtt, cachedFileUri)); 4022 } 4023 } 4024 if (!atts.isEmpty()) { 4025 msg.mAttachments = atts; 4026 msg.mFlagAttachment = true; 4027 if (hasUnloadedAttachments) { 4028 Utility.showToast(context, R.string.message_view_attachment_background_load); 4029 } 4030 } 4031 // Save it or update it... 4032 if (!msg.isSaved()) { 4033 msg.save(context); 4034 } else { 4035 // This is tricky due to how messages/attachments are saved; rather than putz with 4036 // what's changed, we'll delete/re-add them 4037 final ArrayList<ContentProviderOperation> ops = 4038 new ArrayList<ContentProviderOperation>(); 4039 // Delete all existing attachments 4040 ops.add(ContentProviderOperation.newDelete( 4041 ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId)) 4042 .build()); 4043 // Delete the body 4044 ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI) 4045 .withSelection(Body.MESSAGE_KEY + "=?", new String[] {Long.toString(msg.mId)}) 4046 .build()); 4047 // Add the ops for the message, atts, and body 4048 msg.addSaveOps(ops); 4049 // Do it! 4050 try { 4051 applyBatch(ops); 4052 } catch (OperationApplicationException e) { 4053 LogUtils.d(TAG, "applyBatch exception"); 4054 } 4055 } 4056 notifyUIMessage(msg.mId); 4057 4058 if (mailbox.mType == Mailbox.TYPE_OUTBOX) { 4059 startSync(mailbox, 0); 4060 final long originalMsgId = msg.mSourceKey; 4061 if (originalMsgId != 0) { 4062 final Message originalMsg = Message.restoreMessageWithId(context, originalMsgId); 4063 // If the original message exists, set its forwarded/replied to flags 4064 if (originalMsg != null) { 4065 final ContentValues cv = new ContentValues(); 4066 flags = originalMsg.mFlags; 4067 switch(draftType) { 4068 case DraftType.FORWARD: 4069 flags |= Message.FLAG_FORWARDED; 4070 break; 4071 case DraftType.REPLY_ALL: 4072 case DraftType.REPLY: 4073 flags |= Message.FLAG_REPLIED_TO; 4074 break; 4075 } 4076 cv.put(Message.FLAGS, flags); 4077 context.getContentResolver().update(ContentUris.withAppendedId( 4078 Message.CONTENT_URI, originalMsgId), cv, null, null); 4079 } 4080 } 4081 } 4082 return uiUri("uimessage", msg.mId); 4083 } 4084 4085 private Uri uiSaveDraftMessage(final long accountId, final Bundle extras) { 4086 final Mailbox mailbox = 4087 getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_DRAFTS); 4088 if (mailbox == null) return null; 4089 final Message msg; 4090 if (extras.containsKey(BaseColumns._ID)) { 4091 final long messageId = extras.getLong(BaseColumns._ID); 4092 msg = Message.restoreMessageWithId(getContext(), messageId); 4093 } else { 4094 msg = new Message(); 4095 } 4096 return uiSaveMessage(msg, mailbox, extras); 4097 } 4098 4099 private Uri uiSendDraftMessage(final long accountId, final Bundle extras) { 4100 final Context context = getContext(); 4101 final Message msg; 4102 if (extras.containsKey(BaseColumns._ID)) { 4103 final long messageId = extras.getLong(BaseColumns._ID); 4104 msg = Message.restoreMessageWithId(getContext(), messageId); 4105 } else { 4106 msg = new Message(); 4107 } 4108 4109 if (msg == null) return null; 4110 final Mailbox mailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_OUTBOX); 4111 if (mailbox == null) return null; 4112 // Make sure the sent mailbox exists, since it will be necessary soon. 4113 // TODO(yph): move system mailbox creation to somewhere sane. 4114 final Mailbox sentMailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_SENT); 4115 if (sentMailbox == null) return null; 4116 final Uri messageUri = uiSaveMessage(msg, mailbox, extras); 4117 // Kick observers 4118 context.getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 4119 return messageUri; 4120 } 4121 4122 private static void putIntegerLongOrBoolean(ContentValues values, String columnName, 4123 Object value) { 4124 if (value instanceof Integer) { 4125 Integer intValue = (Integer)value; 4126 values.put(columnName, intValue); 4127 } else if (value instanceof Boolean) { 4128 Boolean boolValue = (Boolean)value; 4129 values.put(columnName, boolValue ? 1 : 0); 4130 } else if (value instanceof Long) { 4131 Long longValue = (Long)value; 4132 values.put(columnName, longValue); 4133 } 4134 } 4135 4136 /** 4137 * Update the timestamps for the folders specified and notifies on the recent folder URI. 4138 * @param folders array of folder Uris to update 4139 * @return number of folders updated 4140 */ 4141 private static int updateTimestamp(final Context context, String id, Uri[] folders){ 4142 int updated = 0; 4143 final long now = System.currentTimeMillis(); 4144 final ContentResolver resolver = context.getContentResolver(); 4145 final ContentValues touchValues = new ContentValues(); 4146 for (final Uri folder : folders) { 4147 touchValues.put(MailboxColumns.LAST_TOUCHED_TIME, now); 4148 LogUtils.d(TAG, "updateStamp: %s updated", folder); 4149 updated += resolver.update(folder, touchValues, null, null); 4150 } 4151 final Uri toNotify = 4152 UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build(); 4153 LogUtils.d(TAG, "updateTimestamp: Notifying on %s", toNotify); 4154 resolver.notifyChange(toNotify, null); 4155 return updated; 4156 } 4157 4158 /** 4159 * Updates the recent folders. The values to be updated are specified as ContentValues pairs 4160 * of (Folder URI, access timestamp). Returns nonzero if successful, always. 4161 * @param uri provider query uri 4162 * @param values uri, timestamp pairs 4163 * @return nonzero value always. 4164 */ 4165 private int uiUpdateRecentFolders(Uri uri, ContentValues values) { 4166 final int numFolders = values.size(); 4167 final String id = uri.getPathSegments().get(1); 4168 final Uri[] folders = new Uri[numFolders]; 4169 final Context context = getContext(); 4170 int i = 0; 4171 for (final String uriString : values.keySet()) { 4172 folders[i] = Uri.parse(uriString); 4173 } 4174 return updateTimestamp(context, id, folders); 4175 } 4176 4177 /** 4178 * Populates the recent folders according to the design. 4179 * @param uri provider query uri 4180 * @return the number of recent folders were populated. 4181 */ 4182 private int uiPopulateRecentFolders(Uri uri) { 4183 final Context context = getContext(); 4184 final String id = uri.getLastPathSegment(); 4185 final Uri[] recentFolders = defaultRecentFolders(id); 4186 final int numFolders = recentFolders.length; 4187 if (numFolders <= 0) { 4188 return 0; 4189 } 4190 final int rowsUpdated = updateTimestamp(context, id, recentFolders); 4191 LogUtils.d(TAG, "uiPopulateRecentFolders: %d folders changed", rowsUpdated); 4192 return rowsUpdated; 4193 } 4194 4195 private int uiUpdateAttachment(Uri uri, ContentValues uiValues) { 4196 int result = 0; 4197 Integer stateValue = uiValues.getAsInteger(UIProvider.AttachmentColumns.STATE); 4198 if (stateValue != null) { 4199 // This is a command from UIProvider 4200 long attachmentId = Long.parseLong(uri.getLastPathSegment()); 4201 Context context = getContext(); 4202 Attachment attachment = 4203 Attachment.restoreAttachmentWithId(context, attachmentId); 4204 if (attachment == null) { 4205 // Went away; ah, well... 4206 return result; 4207 } 4208 int state = stateValue; 4209 ContentValues values = new ContentValues(); 4210 if (state == UIProvider.AttachmentState.NOT_SAVED 4211 || state == UIProvider.AttachmentState.REDOWNLOADING) { 4212 // Set state, try to cancel request 4213 values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.NOT_SAVED); 4214 values.put(AttachmentColumns.FLAGS, 4215 attachment.mFlags &= ~Attachment.FLAG_DOWNLOAD_USER_REQUEST); 4216 attachment.update(context, values); 4217 result = 1; 4218 } 4219 if (state == UIProvider.AttachmentState.DOWNLOADING 4220 || state == UIProvider.AttachmentState.REDOWNLOADING) { 4221 // Set state and destination; request download 4222 values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.DOWNLOADING); 4223 Integer destinationValue = 4224 uiValues.getAsInteger(UIProvider.AttachmentColumns.DESTINATION); 4225 values.put(AttachmentColumns.UI_DESTINATION, 4226 destinationValue == null ? 0 : destinationValue); 4227 values.put(AttachmentColumns.FLAGS, 4228 attachment.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST); 4229 attachment.update(context, values); 4230 result = 1; 4231 } 4232 if (state == UIProvider.AttachmentState.SAVED) { 4233 // If this is an inline attachment, notify message has changed 4234 if (!TextUtils.isEmpty(attachment.mContentId)) { 4235 notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, attachment.mMessageKey); 4236 } 4237 result = 1; 4238 } 4239 } 4240 return result; 4241 } 4242 4243 private int uiUpdateFolder(final Context context, Uri uri, ContentValues uiValues) { 4244 // We need to mark seen separately 4245 if (uiValues.containsKey(UIProvider.ConversationColumns.SEEN)) { 4246 final int seenValue = uiValues.getAsInteger(UIProvider.ConversationColumns.SEEN); 4247 4248 if (seenValue == 1) { 4249 final String mailboxId = uri.getLastPathSegment(); 4250 final int rows = markAllSeen(context, mailboxId); 4251 4252 if (uiValues.size() == 1) { 4253 // Nothing else to do, so return this value 4254 return rows; 4255 } 4256 } 4257 } 4258 4259 final Uri ourUri = convertToEmailProviderUri(uri, Mailbox.CONTENT_URI, true); 4260 if (ourUri == null) return 0; 4261 ContentValues ourValues = new ContentValues(); 4262 // This should only be called via update to "recent folders" 4263 for (String columnName: uiValues.keySet()) { 4264 if (columnName.equals(MailboxColumns.LAST_TOUCHED_TIME)) { 4265 ourValues.put(MailboxColumns.LAST_TOUCHED_TIME, uiValues.getAsLong(columnName)); 4266 } 4267 } 4268 return update(ourUri, ourValues, null, null); 4269 } 4270 4271 private int markAllSeen(final Context context, final String mailboxId) { 4272 final SQLiteDatabase db = getDatabase(context); 4273 final String table = Message.TABLE_NAME; 4274 final ContentValues values = new ContentValues(1); 4275 values.put(MessageColumns.FLAG_SEEN, 1); 4276 final String whereClause = MessageColumns.MAILBOX_KEY + " = ?"; 4277 final String[] whereArgs = new String[] {mailboxId}; 4278 4279 return db.update(table, values, whereClause, whereArgs); 4280 } 4281 4282 private ContentValues convertUiMessageValues(Message message, ContentValues values) { 4283 final ContentValues ourValues = new ContentValues(); 4284 for (String columnName : values.keySet()) { 4285 final Object val = values.get(columnName); 4286 if (columnName.equals(UIProvider.ConversationColumns.STARRED)) { 4287 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val); 4288 } else if (columnName.equals(UIProvider.ConversationColumns.READ)) { 4289 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val); 4290 } else if (columnName.equals(UIProvider.ConversationColumns.SEEN)) { 4291 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_SEEN, val); 4292 } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 4293 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val); 4294 } else if (columnName.equals(UIProvider.ConversationOperations.FOLDERS_UPDATED)) { 4295 // Skip this column, as the folders will also be specified the RAW_FOLDERS column 4296 } else if (columnName.equals(UIProvider.ConversationColumns.RAW_FOLDERS)) { 4297 // Convert from folder list uri to mailbox key 4298 final FolderList flist = FolderList.fromBlob(values.getAsByteArray(columnName)); 4299 if (flist.folders.size() != 1) { 4300 LogUtils.e(TAG, 4301 "Incorrect number of folders for this message: Message is %s", 4302 message.mId); 4303 } else { 4304 final Folder f = flist.folders.get(0); 4305 final Uri uri = f.folderUri.fullUri; 4306 final Long mailboxId = Long.parseLong(uri.getLastPathSegment()); 4307 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId); 4308 } 4309 } else if (columnName.equals(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES)) { 4310 Address[] fromList = Address.unpack(message.mFrom); 4311 final MailPrefs mailPrefs = MailPrefs.get(getContext()); 4312 for (Address sender : fromList) { 4313 final String email = sender.getAddress(); 4314 mailPrefs.setDisplayImagesFromSender(email, null); 4315 } 4316 } else if (columnName.equals(UIProvider.ConversationColumns.VIEWED) || 4317 columnName.equals(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO)) { 4318 // Ignore for now 4319 } else if (UIProvider.ConversationColumns.CONVERSATION_INFO.equals(columnName)) { 4320 // Email's conversation info is generated, not stored, so just ignore this update 4321 } else { 4322 throw new IllegalArgumentException("Can't update " + columnName + " in message"); 4323 } 4324 } 4325 return ourValues; 4326 } 4327 4328 private static Uri convertToEmailProviderUri(Uri uri, Uri newBaseUri, boolean asProvider) { 4329 final String idString = uri.getLastPathSegment(); 4330 try { 4331 final long id = Long.parseLong(idString); 4332 Uri ourUri = ContentUris.withAppendedId(newBaseUri, id); 4333 if (asProvider) { 4334 ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build(); 4335 } 4336 return ourUri; 4337 } catch (NumberFormatException e) { 4338 return null; 4339 } 4340 } 4341 4342 private Message getMessageFromLastSegment(Uri uri) { 4343 long messageId = Long.parseLong(uri.getLastPathSegment()); 4344 return Message.restoreMessageWithId(getContext(), messageId); 4345 } 4346 4347 /** 4348 * Add an undo operation for the current sequence; if the sequence is newer than what we've had, 4349 * clear out the undo list and start over 4350 * @param uri the uri we're working on 4351 * @param op the ContentProviderOperation to perform upon undo 4352 */ 4353 private void addToSequence(Uri uri, ContentProviderOperation op) { 4354 String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER); 4355 if (sequenceString != null) { 4356 int sequence = Integer.parseInt(sequenceString); 4357 if (sequence > mLastSequence) { 4358 // Reset sequence 4359 mLastSequenceOps.clear(); 4360 mLastSequence = sequence; 4361 } 4362 // TODO: Need something to indicate a change isn't ready (undoable) 4363 mLastSequenceOps.add(op); 4364 } 4365 } 4366 4367 // TODO: This should depend on flags on the mailbox... 4368 private static boolean uploadsToServer(Context context, Mailbox m) { 4369 if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX || 4370 m.mType == Mailbox.TYPE_SEARCH) { 4371 return false; 4372 } 4373 String protocol = Account.getProtocol(context, m.mAccountKey); 4374 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 4375 return (info != null && info.syncChanges); 4376 } 4377 4378 private int uiUpdateMessage(Uri uri, ContentValues values) { 4379 return uiUpdateMessage(uri, values, false); 4380 } 4381 4382 private int uiUpdateMessage(Uri uri, ContentValues values, boolean forceSync) { 4383 Context context = getContext(); 4384 Message msg = getMessageFromLastSegment(uri); 4385 if (msg == null) return 0; 4386 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 4387 if (mailbox == null) return 0; 4388 Uri ourBaseUri = 4389 (forceSync || uploadsToServer(context, mailbox)) ? Message.SYNCED_CONTENT_URI : 4390 Message.CONTENT_URI; 4391 Uri ourUri = convertToEmailProviderUri(uri, ourBaseUri, true); 4392 if (ourUri == null) return 0; 4393 4394 // Special case - meeting response 4395 if (values.containsKey(UIProvider.MessageOperations.RESPOND_COLUMN)) { 4396 final EmailServiceProxy service = 4397 EmailServiceUtils.getServiceForAccount(context, mailbox.mAccountKey); 4398 try { 4399 service.sendMeetingResponse(msg.mId, 4400 values.getAsInteger(UIProvider.MessageOperations.RESPOND_COLUMN)); 4401 // Delete the message immediately 4402 uiDeleteMessage(uri); 4403 Utility.showToast(context, R.string.confirm_response); 4404 // Notify box has changed so the deletion is reflected in the UI 4405 notifyUIConversationMailbox(mailbox.mId); 4406 } catch (RemoteException e) { 4407 LogUtils.d(TAG, "Remote exception while sending meeting response"); 4408 } 4409 return 1; 4410 } 4411 4412 // Another special case - deleting a draft. 4413 final String operation = values.getAsString( 4414 UIProvider.ConversationOperations.OPERATION_KEY); 4415 if (UIProvider.ConversationOperations.DISCARD_DRAFTS.equals(operation)) { 4416 uiDeleteMessage(uri); 4417 return 1; 4418 } 4419 4420 ContentValues undoValues = new ContentValues(); 4421 ContentValues ourValues = convertUiMessageValues(msg, values); 4422 for (String columnName: ourValues.keySet()) { 4423 if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 4424 undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey); 4425 } else if (columnName.equals(MessageColumns.FLAG_READ)) { 4426 undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead); 4427 } else if (columnName.equals(MessageColumns.FLAG_SEEN)) { 4428 undoValues.put(MessageColumns.FLAG_SEEN, msg.mFlagSeen); 4429 } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) { 4430 undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite); 4431 } 4432 } 4433 if (undoValues.size() == 0) { 4434 return -1; 4435 } 4436 final Boolean suppressUndo = 4437 values.getAsBoolean(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO); 4438 if (suppressUndo == null || !suppressUndo) { 4439 final ContentProviderOperation op = 4440 ContentProviderOperation.newUpdate(convertToEmailProviderUri( 4441 uri, ourBaseUri, false)) 4442 .withValues(undoValues) 4443 .build(); 4444 addToSequence(uri, op); 4445 } 4446 4447 return update(ourUri, ourValues, null, null); 4448 } 4449 4450 /** 4451 * Projection for use with getting mailbox & account keys for a message. 4452 */ 4453 private static final String[] MESSAGE_KEYS_PROJECTION = 4454 { MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY }; 4455 private static final int MESSAGE_KEYS_MAILBOX_KEY_COLUMN = 0; 4456 private static final int MESSAGE_KEYS_ACCOUNT_KEY_COLUMN = 1; 4457 4458 /** 4459 * Notify necessary UI components in response to a message update. 4460 * @param uri The {@link Uri} for this message update. 4461 * @param messageId The id of the message that's been updated. 4462 * @param values The {@link ContentValues} that were updated in the message. 4463 */ 4464 private void handleMessageUpdateNotifications(final Uri uri, final String messageId, 4465 final ContentValues values) { 4466 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 4467 notifyUIConversation(uri); 4468 } 4469 // TODO: Ideally, also test that the values actually changed. 4470 if (values.containsKey(MessageColumns.FLAG_READ) || 4471 values.containsKey(MessageColumns.MAILBOX_KEY)) { 4472 final Cursor c = query( 4473 Message.CONTENT_URI.buildUpon().appendEncodedPath(messageId).build(), 4474 MESSAGE_KEYS_PROJECTION, null, null, null); 4475 if (c != null) { 4476 try { 4477 if (c.moveToFirst()) { 4478 notifyUIFolder(c.getLong(MESSAGE_KEYS_MAILBOX_KEY_COLUMN), 4479 c.getLong(MESSAGE_KEYS_ACCOUNT_KEY_COLUMN)); 4480 } 4481 } finally { 4482 c.close(); 4483 } 4484 } 4485 } 4486 } 4487 4488 /** 4489 * Perform a "Delete" operation 4490 * @param uri message to delete 4491 * @return number of rows affected 4492 */ 4493 private int uiDeleteMessage(Uri uri) { 4494 final Context context = getContext(); 4495 Message msg = getMessageFromLastSegment(uri); 4496 if (msg == null) return 0; 4497 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 4498 if (mailbox == null) return 0; 4499 if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_DRAFTS) { 4500 // We actually delete these, including attachments 4501 AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId); 4502 final int r = context.getContentResolver().delete( 4503 ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg.mId), null, null); 4504 notifyUIFolder(mailbox.mId, mailbox.mAccountKey); 4505 return r; 4506 } 4507 Mailbox trashMailbox = 4508 Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH); 4509 if (trashMailbox == null) { 4510 return 0; 4511 } 4512 ContentValues values = new ContentValues(); 4513 values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId); 4514 final int r = uiUpdateMessage(uri, values, true); 4515 notifyUIFolder(mailbox.mId, mailbox.mAccountKey); 4516 return r; 4517 } 4518 4519 public static final String PICKER_UI_ACCOUNT = "picker_ui_account"; 4520 public static final String PICKER_MAILBOX_TYPE = "picker_mailbox_type"; 4521 // Currently unused 4522 //public static final String PICKER_MESSAGE_ID = "picker_message_id"; 4523 public static final String PICKER_HEADER_ID = "picker_header_id"; 4524 4525 private int pickFolder(Uri uri, int type, int headerId) { 4526 Context context = getContext(); 4527 Long acctId = Long.parseLong(uri.getLastPathSegment()); 4528 // For push imap, for example, we want the user to select the trash mailbox 4529 Cursor ac = query(uiUri("uiaccount", acctId), UIProvider.ACCOUNTS_PROJECTION, 4530 null, null, null); 4531 try { 4532 if (ac.moveToFirst()) { 4533 final com.android.mail.providers.Account uiAccount = 4534 new com.android.mail.providers.Account(ac); 4535 Intent intent = new Intent(context, FolderPickerActivity.class); 4536 intent.putExtra(PICKER_UI_ACCOUNT, uiAccount); 4537 intent.putExtra(PICKER_MAILBOX_TYPE, type); 4538 intent.putExtra(PICKER_HEADER_ID, headerId); 4539 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 4540 context.startActivity(intent); 4541 return 1; 4542 } 4543 return 0; 4544 } finally { 4545 ac.close(); 4546 } 4547 } 4548 4549 private int pickTrashFolder(Uri uri) { 4550 return pickFolder(uri, Mailbox.TYPE_TRASH, R.string.trash_folder_selection_title); 4551 } 4552 4553 private int pickSentFolder(Uri uri) { 4554 return pickFolder(uri, Mailbox.TYPE_SENT, R.string.sent_folder_selection_title); 4555 } 4556 4557 private Cursor uiUndo(String[] projection) { 4558 // First see if we have any operations saved 4559 // TODO: Make sure seq matches 4560 if (!mLastSequenceOps.isEmpty()) { 4561 try { 4562 // TODO Always use this projection? Or what's passed in? 4563 // Not sure if UI wants it, but I'm making a cursor of convo uri's 4564 MatrixCursor c = new MatrixCursorWithCachedColumns( 4565 new String[] {UIProvider.ConversationColumns.URI}, 4566 mLastSequenceOps.size()); 4567 for (ContentProviderOperation op: mLastSequenceOps) { 4568 c.addRow(new String[] {op.getUri().toString()}); 4569 } 4570 // Just apply the batch and we're done! 4571 applyBatch(mLastSequenceOps); 4572 // But clear the operations 4573 mLastSequenceOps.clear(); 4574 return c; 4575 } catch (OperationApplicationException e) { 4576 LogUtils.d(TAG, "applyBatch exception"); 4577 } 4578 } 4579 return new MatrixCursorWithCachedColumns(projection, 0); 4580 } 4581 4582 private void notifyUIConversation(Uri uri) { 4583 String id = uri.getLastPathSegment(); 4584 Message msg = Message.restoreMessageWithId(getContext(), Long.parseLong(id)); 4585 if (msg != null) { 4586 notifyUIConversationMailbox(msg.mMailboxKey); 4587 } 4588 } 4589 4590 /** 4591 * Notify about the Mailbox id passed in 4592 * @param id the Mailbox id to be notified 4593 */ 4594 private void notifyUIConversationMailbox(long id) { 4595 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(id)); 4596 Mailbox mailbox = Mailbox.restoreMailboxWithId(getContext(), id); 4597 if (mailbox == null) { 4598 LogUtils.w(TAG, "No mailbox for notification: " + id); 4599 return; 4600 } 4601 // Notify combined inbox... 4602 if (mailbox.mType == Mailbox.TYPE_INBOX) { 4603 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, 4604 EmailProvider.combinedMailboxId(Mailbox.TYPE_INBOX)); 4605 } 4606 notifyWidgets(id); 4607 } 4608 4609 /** 4610 * Notify about the message id passed in 4611 * @param id the message id to be notified 4612 */ 4613 private void notifyUIMessage(long id) { 4614 notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, id); 4615 } 4616 4617 /** 4618 * Notify about the Account id passed in 4619 * @param id the Account id to be notified 4620 */ 4621 private void notifyUIAccount(long id) { 4622 // Notify on the specific account 4623 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, Long.toString(id)); 4624 4625 // Notify on the all accounts list 4626 notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 4627 } 4628 4629 /** 4630 * Notify about a folder update. Because folder changes can affect the conversation cursor's 4631 * extras, the conversation must also be notified here. 4632 * @param folderId the folder id to be notified 4633 * @param accountId the account id to be notified (for folder list notification). 4634 */ 4635 private void notifyUIFolder(final String folderId, final long accountId) { 4636 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, folderId); 4637 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, folderId); 4638 if (accountId != Account.NO_ACCOUNT) { 4639 notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId); 4640 } 4641 4642 // Notify for combined account too 4643 // TODO: might be nice to only notify when an inbox changes 4644 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, 4645 getVirtualMailboxId(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX)); 4646 notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, COMBINED_ACCOUNT_ID); 4647 } 4648 4649 private void notifyUIFolder(final long folderId, final long accountId) { 4650 notifyUIFolder(Long.toString(folderId), accountId); 4651 } 4652 4653 private void notifyUI(Uri uri, String id) { 4654 final Uri notifyUri = (id != null) ? uri.buildUpon().appendPath(id).build() : uri; 4655 getContext().getContentResolver().notifyChange(notifyUri, null); 4656 } 4657 4658 private void notifyUI(Uri uri, long id) { 4659 notifyUI(uri, Long.toString(id)); 4660 } 4661 4662 private Mailbox getMailbox(final Uri uri) { 4663 final long id = Long.parseLong(uri.getLastPathSegment()); 4664 return Mailbox.restoreMailboxWithId(getContext(), id); 4665 } 4666 4667 /** 4668 * Create an android.accounts.Account object for this account. 4669 * @param accountId id of account to load. 4670 * @return an android.accounts.Account for this account, or null if we can't load it. 4671 */ 4672 private android.accounts.Account getAccountManagerAccount(final long accountId) { 4673 final Context context = getContext(); 4674 final Account account = Account.restoreAccountWithId(context, accountId); 4675 if (account == null) return null; 4676 EmailServiceInfo info = 4677 EmailServiceUtils.getServiceInfo(context, account.getProtocol(context)); 4678 return new android.accounts.Account(account.mEmailAddress, info.accountType); 4679 } 4680 4681 /** 4682 * Update an account's periodic sync if the sync interval has changed. 4683 * @param accountId id for the account to update. 4684 * @param values the ContentValues for this update to the account. 4685 */ 4686 private void updateAccountSyncInterval(final long accountId, final ContentValues values) { 4687 final Integer syncInterval = values.getAsInteger(AccountColumns.SYNC_INTERVAL); 4688 if (syncInterval == null) { 4689 // No change to the sync interval. 4690 return; 4691 } 4692 final android.accounts.Account account = getAccountManagerAccount(accountId); 4693 if (account == null) { 4694 // Unable to load the account, or unknown protocol. 4695 return; 4696 } 4697 4698 LogUtils.d(TAG, "Setting sync interval for account " + accountId + " to " + syncInterval + 4699 " minutes"); 4700 4701 // First remove all existing periodic syncs. 4702 final List<PeriodicSync> syncs = 4703 ContentResolver.getPeriodicSyncs(account, EmailContent.AUTHORITY); 4704 for (final PeriodicSync sync : syncs) { 4705 ContentResolver.removePeriodicSync(account, EmailContent.AUTHORITY, sync.extras); 4706 } 4707 4708 // Only positive values of sync interval indicate periodic syncs. The value is in minutes, 4709 // while addPeriodicSync expects its time in seconds. 4710 if (syncInterval > 0) { 4711 ContentResolver.addPeriodicSync(account, EmailContent.AUTHORITY, Bundle.EMPTY, 4712 syncInterval * DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS); 4713 } 4714 } 4715 4716 private void startSync(final Mailbox mailbox, final int deltaMessageCount) { 4717 android.accounts.Account account = getAccountManagerAccount(mailbox.mAccountKey); 4718 Bundle extras = new Bundle(7); 4719 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 4720 extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true); 4721 extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); 4722 extras.putLong(Mailbox.SYNC_EXTRA_MAILBOX_ID, mailbox.mId); 4723 if (deltaMessageCount != 0) { 4724 extras.putInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, deltaMessageCount); 4725 } 4726 extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_URI, 4727 EmailContent.CONTENT_URI.toString()); 4728 extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD, 4729 SYNC_STATUS_CALLBACK_METHOD); 4730 ContentResolver.requestSync(account, EmailContent.AUTHORITY, extras); 4731 } 4732 4733 private Cursor uiFolderRefresh(final Mailbox mailbox, final int deltaMessageCount) { 4734 if (mailbox != null) { 4735 startSync(mailbox, deltaMessageCount); 4736 } 4737 return null; 4738 } 4739 4740 //Number of additional messages to load when a user selects "Load more..." in POP/IMAP boxes 4741 public static final int VISIBLE_LIMIT_INCREMENT = 10; 4742 //Number of additional messages to load when a user selects "Load more..." in a search 4743 public static final int SEARCH_MORE_INCREMENT = 10; 4744 4745 private Cursor uiFolderLoadMore(final Mailbox mailbox) { 4746 if (mailbox == null) return null; 4747 if (mailbox.mType == Mailbox.TYPE_SEARCH) { 4748 // Ask for 10 more messages 4749 mSearchParams.mOffset += SEARCH_MORE_INCREMENT; 4750 runSearchQuery(getContext(), mailbox.mAccountKey, mailbox.mId); 4751 } else { 4752 uiFolderRefresh(mailbox, VISIBLE_LIMIT_INCREMENT); 4753 } 4754 return null; 4755 } 4756 4757 private static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__"; 4758 private SearchParams mSearchParams; 4759 4760 /** 4761 * Returns the search mailbox for the specified account, creating one if necessary 4762 * @return the search mailbox for the passed in account 4763 */ 4764 private Mailbox getSearchMailbox(long accountId) { 4765 Context context = getContext(); 4766 Mailbox m = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SEARCH); 4767 if (m == null) { 4768 m = new Mailbox(); 4769 m.mAccountKey = accountId; 4770 m.mServerId = SEARCH_MAILBOX_SERVER_ID; 4771 m.mFlagVisible = false; 4772 m.mDisplayName = SEARCH_MAILBOX_SERVER_ID; 4773 m.mSyncInterval = 0; 4774 m.mType = Mailbox.TYPE_SEARCH; 4775 m.mFlags = Mailbox.FLAG_HOLDS_MAIL; 4776 m.mParentKey = Mailbox.NO_MAILBOX; 4777 m.save(context); 4778 } 4779 return m; 4780 } 4781 4782 private void runSearchQuery(final Context context, final long accountId, 4783 final long searchMailboxId) { 4784 LogUtils.d(TAG, "runSearchQuery. account: %d mailbox id: %d", 4785 accountId, searchMailboxId); 4786 4787 // Start the search running in the background 4788 new AsyncTask<Void, Void, Void>() { 4789 @Override 4790 public Void doInBackground(Void... params) { 4791 final EmailServiceProxy service = 4792 EmailServiceUtils.getServiceForAccount(context, accountId); 4793 if (service != null) { 4794 try { 4795 // Save away the total count 4796 mSearchParams.mTotalCount = 4797 service.searchMessages(accountId, mSearchParams, searchMailboxId); 4798 LogUtils.d(TAG, "EmailProvider#runSearchQuery. TotalCount to UI: %d", 4799 mSearchParams.mTotalCount); 4800 notifyUIFolder(searchMailboxId, accountId); 4801 } catch (RemoteException e) { 4802 LogUtils.e("searchMessages", "RemoteException", e); 4803 } 4804 } 4805 return null; 4806 } 4807 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 4808 } 4809 4810 // TODO: Handle searching for more... 4811 private Cursor uiSearch(Uri uri, String[] projection) { 4812 LogUtils.d(TAG, "runSearchQuery in search %s", uri); 4813 final long accountId = Long.parseLong(uri.getLastPathSegment()); 4814 4815 // TODO: Check the actual mailbox 4816 Mailbox inbox = Mailbox.restoreMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX); 4817 if (inbox == null) { 4818 LogUtils.w(Logging.LOG_TAG, "In uiSearch, inbox doesn't exist for account " 4819 + accountId); 4820 4821 return null; 4822 } 4823 4824 String filter = uri.getQueryParameter(UIProvider.SearchQueryParameters.QUERY); 4825 if (filter == null) { 4826 throw new IllegalArgumentException("No query parameter in search query"); 4827 } 4828 4829 // Find/create our search mailbox 4830 Mailbox searchMailbox = getSearchMailbox(accountId); 4831 final long searchMailboxId = searchMailbox.mId; 4832 4833 mSearchParams = new SearchParams(inbox.mId, filter, searchMailboxId); 4834 4835 final Context context = getContext(); 4836 if (mSearchParams.mOffset == 0) { 4837 LogUtils.d(TAG, "deleting existing search results."); 4838 4839 // Delete existing contents of search mailbox 4840 ContentResolver resolver = context.getContentResolver(); 4841 resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId, 4842 null); 4843 final ContentValues cv = new ContentValues(1); 4844 // For now, use the actual query as the name of the mailbox 4845 cv.put(Mailbox.DISPLAY_NAME, mSearchParams.mFilter); 4846 resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), 4847 cv, null, null); 4848 } 4849 4850 // Start the search running in the background 4851 runSearchQuery(context, accountId, searchMailboxId); 4852 4853 // This will look just like a "normal" folder 4854 return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI, 4855 searchMailbox.mId), projection, false); 4856 } 4857 4858 private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; 4859 4860 /** 4861 * Delete an account and clean it up 4862 */ 4863 private int uiDeleteAccount(Uri uri) { 4864 Context context = getContext(); 4865 long accountId = Long.parseLong(uri.getLastPathSegment()); 4866 try { 4867 // Get the account URI. 4868 final Account account = Account.restoreAccountWithId(context, accountId); 4869 if (account == null) { 4870 return 0; // Already deleted? 4871 } 4872 4873 deleteAccountData(context, accountId); 4874 4875 // Now delete the account itself 4876 uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 4877 context.getContentResolver().delete(uri, null, null); 4878 4879 // Clean up 4880 AccountBackupRestore.backup(context); 4881 SecurityPolicy.getInstance(context).reducePolicies(); 4882 MailActivityEmail.setServicesEnabledSync(context); 4883 return 1; 4884 } catch (Exception e) { 4885 LogUtils.w(Logging.LOG_TAG, "Exception while deleting account", e); 4886 } 4887 return 0; 4888 } 4889 4890 private int uiDeleteAccountData(Uri uri) { 4891 Context context = getContext(); 4892 long accountId = Long.parseLong(uri.getLastPathSegment()); 4893 // Get the account URI. 4894 final Account account = Account.restoreAccountWithId(context, accountId); 4895 if (account == null) { 4896 return 0; // Already deleted? 4897 } 4898 deleteAccountData(context, accountId); 4899 return 1; 4900 } 4901 4902 /** Projection used for getting email address for an account. */ 4903 private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS }; 4904 4905 private static void deleteAccountData(Context context, long accountId) { 4906 // We will delete PIM data, but by the time the asynchronous call to do that happens, 4907 // the account may have been deleted from the DB. Therefore we have to get the email 4908 // address now and send that, rather than the account id. 4909 final String emailAddress = Utility.getFirstRowString(context, Account.CONTENT_URI, 4910 ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION, 4911 new String[] {Long.toString(accountId)}, null, 0); 4912 if (emailAddress == null) { 4913 LogUtils.e(TAG, "Could not find email address for account %d", accountId); 4914 } 4915 4916 // Delete synced attachments 4917 AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId); 4918 4919 // Delete all mailboxes. 4920 ContentResolver resolver = context.getContentResolver(); 4921 String[] accountIdArgs = new String[] { Long.toString(accountId) }; 4922 resolver.delete(Mailbox.CONTENT_URI, MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs); 4923 4924 // Delete account sync key. 4925 final ContentValues cv = new ContentValues(); 4926 cv.putNull(Account.SYNC_KEY); 4927 resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs); 4928 4929 // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable 4930 if (emailAddress != null) { 4931 final IEmailService service = 4932 EmailServiceUtils.getServiceForAccount(context, accountId); 4933 if (service != null) { 4934 try { 4935 service.deleteAccountPIMData(emailAddress); 4936 } catch (final RemoteException e) { 4937 // Can't do anything about this 4938 } 4939 } 4940 } 4941 } 4942 4943 private int[] mSavedWidgetIds = new int[0]; 4944 private final ArrayList<Long> mWidgetNotifyMailboxes = new ArrayList<Long>(); 4945 private AppWidgetManager mAppWidgetManager; 4946 private ComponentName mEmailComponent; 4947 4948 private void notifyWidgets(long mailboxId) { 4949 Context context = getContext(); 4950 // Lazily initialize these 4951 if (mAppWidgetManager == null) { 4952 mAppWidgetManager = AppWidgetManager.getInstance(context); 4953 mEmailComponent = new ComponentName(context, WidgetProvider.PROVIDER_NAME); 4954 } 4955 4956 // See if we have to populate our array of mailboxes used in widgets 4957 int[] widgetIds = mAppWidgetManager.getAppWidgetIds(mEmailComponent); 4958 if (!Arrays.equals(widgetIds, mSavedWidgetIds)) { 4959 mSavedWidgetIds = widgetIds; 4960 String[][] widgetInfos = BaseWidgetProvider.getWidgetInfo(context, widgetIds); 4961 // widgetInfo now has pairs of account uri/folder uri 4962 mWidgetNotifyMailboxes.clear(); 4963 for (String[] widgetInfo: widgetInfos) { 4964 try { 4965 if (widgetInfo == null) continue; 4966 long id = Long.parseLong(Uri.parse(widgetInfo[1]).getLastPathSegment()); 4967 if (!isCombinedMailbox(id)) { 4968 // For a regular mailbox, just add it to the list 4969 if (!mWidgetNotifyMailboxes.contains(id)) { 4970 mWidgetNotifyMailboxes.add(id); 4971 } 4972 } else { 4973 switch (getVirtualMailboxType(id)) { 4974 // We only handle the combined inbox in widgets 4975 case Mailbox.TYPE_INBOX: 4976 Cursor c = query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION, 4977 MailboxColumns.TYPE + "=?", 4978 new String[] {Integer.toString(Mailbox.TYPE_INBOX)}, null); 4979 try { 4980 while (c.moveToNext()) { 4981 mWidgetNotifyMailboxes.add( 4982 c.getLong(Mailbox.ID_PROJECTION_COLUMN)); 4983 } 4984 } finally { 4985 c.close(); 4986 } 4987 break; 4988 } 4989 } 4990 } catch (NumberFormatException e) { 4991 // Move along 4992 } 4993 } 4994 } 4995 4996 // If our mailbox needs to be notified, do so... 4997 if (mWidgetNotifyMailboxes.contains(mailboxId)) { 4998 Intent intent = new Intent(Utils.ACTION_NOTIFY_DATASET_CHANGED); 4999 intent.putExtra(Utils.EXTRA_FOLDER_URI, uiUri("uifolder", mailboxId)); 5000 intent.setType(EMAIL_APP_MIME_TYPE); 5001 context.sendBroadcast(intent); 5002 } 5003 } 5004 5005 @Override 5006 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 5007 Context context = getContext(); 5008 writer.println("Installed services:"); 5009 for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(context)) { 5010 writer.println(" " + info); 5011 } 5012 writer.println(); 5013 writer.println("Accounts: "); 5014 Cursor cursor = query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, null); 5015 if (cursor.getCount() == 0) { 5016 writer.println(" None"); 5017 } 5018 try { 5019 while (cursor.moveToNext()) { 5020 Account account = new Account(); 5021 account.restore(cursor); 5022 writer.println(" Account " + account.mDisplayName); 5023 HostAuth hostAuth = 5024 HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); 5025 if (hostAuth != null) { 5026 writer.println(" Protocol = " + hostAuth.mProtocol + 5027 (TextUtils.isEmpty(account.mProtocolVersion) ? "" : " version " + 5028 account.mProtocolVersion)); 5029 } 5030 } 5031 } finally { 5032 cursor.close(); 5033 } 5034 } 5035} 5036