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