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